Templates and type-safe string formatting based on Swift 5 string interpolation.
- Swift 5 toolchain
Swift string interpolation already allows to use plain strings for the purpose of templates, i.e. you can not just inject value in a string, but use it with conditional and functional operators like ?:
and map
, which allows to express more complex cases common for templates:
"Hello \(names.map{ $0.capitalized }.joined(separator: ", "))!"
// Hello Foo, Bar!
Swift 5 string interpolation improvements allow to extend strings with a DSL suitable for templating, i.e. you can define a for-loop function which will allow a bit more control of interation progress than map
(though it's possible to achieve the same with just map
such expressions can become a bit complicated):
"Hello \(for: names, do: { name, loop in
"\(name)\(loop.index + 1 == loop.length - 1 ? " and " : ", ")"
})!"
// Hello Foo, Bar and FooBar!
This package supports following features:
- default value for optional variable
\(_: String?, default: String)
for
loops\(for: [T], where: (T) -> Bool, do: (T, LoopContext) -> Void, empty: @autoclosure () -> Template, join: (LoopContext) -> Template, keepEmptyLines: Bool)
- embedding other templates
\(_: Template)
or\(include: String, notFound: @autoclosure () -> Template)
- indentation
\(indent: Int, with: String, indentFirstLine: Bool, keepEmptyLines: Bool, _: @autoclosure () -> Template)
- trimming whitespaces or new lines
\(trim: CharacterSet, _: TrimDirection)
,\(_trim: CharacterSet)
,\(trim_: CharacterSet)
,(_trim_: CharacterSet)
- templates inheritance based on Swift class inheritance
Another application of string interpolation allows to implement type-safe (almost) string format API that will ensure that wrong type of parameter passed in to build the final string. implementation of this API is heavily based on ApplicativeRouter by Point-Free.
One way of using it is using operators:
let hello = "Hello, " %> param(.string)
hello.render("Swift")
This will create a Format<String>
, string formatter that will accept single String
argument. Note that type of formatter can be dropped here - it will be inferred from type of parameter passed to param
function.
Alternativy you can use string interpolation:
let hello: Format<String> = "Hello, \(.string)!"
hello.render("Swift")
This will create the same type of formatter, but type declaration is required here. This kind of formatter will not be type-safe in the same way as the first one. If the wrong type is used, i.e. if it is defined as Format<Int>
instead, the code will compile but it will raise a runtime exception when rendering.
let hello: Format<Int> = "Hello, \(.string)!"
hello.render(0) // runtime error: Could not cast value of type 'Swift.Int' to 'Swift.String'
Similarly to Format
you can use StringFormat
to create C-style strings formats, that can be then localized:
let hello: StringFormat<String> = "Hello, \(.string)!"
hello.render(templateFor: "Swift")
// Hello, %@!
hello.localized("Swift")
// Olá, Swift!
Internally localized
function will call Bundle.localizedString
method to get localized format string and will pass it as well as string parameter to String(format:arguments:)
method to produce the final string.
To build strongly typed string formats with operators use sparam
and slit
functions instead of param
and lit
:
let hello = "Hello, " %> sparam(.string)
hello.render(templateFor: "Swift")
// Hello, %@!
hello.localized("Swift")
// Olá, Swift!
Template
and Format
can work together in an interesting way. If you have both a template and a formatter for the same string you can use the formatter to extract values from the template to find out values used to render it.
let name = "world"
let format: Format<String> = "Hello, \(.string)."
let template: Template = "Hello, \(name)."
let name = format.match(template)
//name = "world"
This is similar to matching regular expressions, but in a type-safe way. Formatter can also output a template-like string:
format.render(templateFor: name)
//Hello, \(String).
To create a template you define a subclass of Renderer
class with a template markup in its template
property and then you use this renderer to render the template. There you can access any variables you passed in the constructor or computed variables which replaces the notion of context
and blocks
common in other template engines.
class HelloWorld: Renderer {
let names: [String]
init(names: [String]) {
self.names = names
}
var greetings: Template {
return "Hello"
}
override var template: Template {
return "\(greetings) \(names.map{ $0.capitalized }.joined(separator: ", "))!"
}
}
let content = HelloWorld(names: ["Foo", "Bar"]).render()
//Hello Foo, Bar!
To create a string format you define a value of type Format
. If format uses two arguments, A
and B
, then formatter will expect a single argument of type (A, B)
. In case of three arguments in the format, A
, B
and C
, the type of the argument will be (A, (B, C))
and so on. So as you can see types of individual arguments are grouped in pairs alligned to the right side. When rendering this format into string you can pass all parameters as an arguments list using parenthesize
free function:
let format: Format<(String, (Int, (String, Int)))> =
"Hello, \(.string). Today is \(.int) of \(.string) \(.int)"
let result = format.render(parenthesize("world", 14, "Jan", 2019))
//Hello, world. Today is 14 of Jan 2019
To run tests run swift test
.
You can install this package with Swift Package Manager.