Table of Contents
- Expressions
- Contents
- Where Kotlin appears
- How the compiler reads Kotlin
- Text expressions
- Attribute expressions
- Component attributes
- Markup literals inside Kotlin
- Directive expressions
- Names and scope
- Nullability and smart casts
- Evaluation order and control flow
- Whitespace around expressions
- Literal characters and escaping
- Errors
Expressions
A .kmx file is markup with Kotlin in it. This page defines exactly where Kotlin can appear, how the compiler finds where each piece of Kotlin ends, what types each position accepts, which names an expression can use, and what gets written to the output.
The rules describe KMX 0.1.1. The Generated Code page shows the Kotlin these rules produce.
Contents
- Where Kotlin appears
- How the compiler reads Kotlin
- Text expressions
- Attribute expressions
- Component attributes
- Markup literals inside Kotlin
- Directive expressions
- Names and scope
- Nullability and smart casts
- Evaluation order and control flow
- Whitespace around expressions
- Literal characters and escaping
- Errors
Where Kotlin appears
| Position | Syntax | Must be | Becomes |
|---|---|---|---|
| Header code | any line before the markup that is not package, import, props or component |
Kotlin statements | copied, in order, before the markup |
| Text expression | {expr} between tags |
an expression of a text type | insert(expr) |
| Attribute expression | name={expr} on an element |
String?, Number? or Boolean? |
attribute("name", expr) |
| Interpolated attribute | name="a{expr}b" |
String, Number, Char or Boolean per {} |
attribute("name", "a" + interpolate(expr) + "b") |
| Attribute spread | {...expr} on an element |
Map<String, String?> |
attributes(expr) |
| Component attribute | name={expr} on a component tag |
the parameter's type | named argument name = expr |
| Directive head | @if (expr), else if (expr), @for (header), @when (subject) |
what Kotlin's if, for, when accept |
if (expr), for (header), when (subject) |
@when branch condition |
cond -> ... |
what a Kotlin when branch accepts |
cond -> { ... } |
| Statement block | @{ statements } |
Kotlin statements | copied at that position |
Kotlin is copied as written. KMX only finds where each piece ends and replaces the markup literals inside it; kotlinc checks everything else.
How the compiler reads Kotlin
The compiler scans Kotlin without parsing it. While scanning, it skips over:
- string literals,
"..."and raw"""...""", including a run of quotes that ends a raw string (""""), - string templates
${...}, scanned recursively as Kotlin, - character literals
'...', with\escapes, - line comments
// ...up to the end of the line, - block comments
/* ... */, which nest as in Kotlin, - balanced
( ),[ ]and{ }.
A }, ) or ] inside any of these does not end the code. The code ends at:
| Context | End |
|---|---|
{...} in text, ={...}, {...} in an interpolated value, {...spread}, @{...} |
the } that closes the opening brace |
( ... ) after @if, else if, @for, @when |
the ) that closes the opening parenthesis |
@when branch condition |
the first -> outside brackets |
| Header statement | the end of the line, outside brackets and comments; a statement continues onto the next lines while a bracket or a block comment is open |
Consequences:
{"}" + '}'}is one expression. So is{items.map { "<b>" }}.- A line comment runs to the end of the line, so
{name // note}never closes. Put the}on the next line, or use/* note */. - A header statement that continues with a binary operator at the start of the next line, outside brackets, ends at the line break. Wrap it in parentheses or end the line with the operator.
Text expressions
{expr} in text writes the value of expr. The compiler emits insert(expr), and insert accepts only these types:
| Static type | Output |
|---|---|
String |
the string as text, escaped; an empty string adds nothing |
Number (Int, Long, Double, BigDecimal, ...) |
toString(), escaped: 1.0, 1000. Not locale-aware |
Char |
the character |
Boolean |
true or false |
Node? (Element, Text, Comment, Fragment, Markup, or a markup literal) |
the node; a Fragment is flattened into its parent; null adds nothing |
Iterable<Node> (List<Element>, Set<Node>, ...) |
every node, in iteration order |
Any other type does not compile, including:
String?,Int?and every other nullable non-node type: decide whatnullprints, as in{nickname ?: name}.- enums, dates, and other objects: convert them explicitly, as in
{status.name}or with a formatting function. Sequence<Node>,Array<Node>andIterable<String>: convert with.toList()or.map { ... }to nodes.Unit, such as a call to a function that returns nothing.
{} that holds only whitespace and comments, such as {/* TODO */}, produces nothing.
<p>{name}, {count} items, {if (vip) "VIP" else ""}</p>
<ul>{tags.map { <li>{it}</li> }}</ul>
{coupon?.let { <p>Coupon: {it.code}</p> }}
{Markup.trusted("<b>bold</b>")} {/* with import xyz.nillpoe.kmx.Markup */}
Attribute expressions
On an element (a tag whose name starts with a lowercase letter or _), an attribute is one of:
| Syntax | Emitted | Accepted types |
|---|---|---|
name="text" |
attribute("name", "text") |
literal; entities decoded |
name='text' |
same | literal |
name={expr} |
attribute("name", expr) |
String?, Number?, Boolean? |
name="a{expr}b" |
attribute("name", ("a" + interpolate(expr) + "b")) |
String, Number, Char, Boolean in each {} |
name |
attribute("name", true) |
none |
{...expr} |
attributes(expr) |
Map<String, String?> |
What each value writes:
| Value | Output |
|---|---|
a String |
name="value", escaped |
a Number |
name=" + toString() + " |
true |
name="name" (the XML form of a boolean attribute) |
false or null |
no attribute, and any earlier value of name on this element is removed |
a map entry k to v |
as attribute(k, v), in map order |
Attributes are set in source order and the last value of a name wins. Put {...defaults} first to let later attributes override it, or last to let it override them.
In an interpolated value, {{ and }} are literal braces, entities are decoded in the literal parts, and each {expr} must be non-null. A value without { is a literal. Interpolation never omits the attribute: title="{a}" with a == "" writes title="".
Component attributes
A tag is a component when the first character after its last . is uppercase: <Button>, <ui.Button>. It compiles to a call of a function with that name, Button(...) or ui.Button(...), whose result is inserted like a text expression of type Node.
| Syntax | Argument |
|---|---|
name="text" |
name = "text" |
name={expr} |
name = expr, any type the parameter takes |
name="a{expr}b" |
name = ("a" + interpolate(expr) + "b") |
name |
name = true |
| content between the tags | children = markup { ... }, of type Node |
{...expr} |
not allowed |
Attribute names must be Kotlin identifiers, so data-x is an error on a component. A literal is always a String: write size={3}, not size="3", for an Int parameter. When the tag has no content, or only whitespace that the whitespace rules remove, children is not passed and the parameter's default applies. Other Node parameters take markup literals: <Card header={<h2>Shipping</h2>}>.
Markup literals inside Kotlin
Inside any Kotlin position, < starts a markup literal when both hold:
-
The next character is a letter,
_or>(a fragment<>...</>). -
The
<is where an expression can start. That is decided by the text before it, after trailing whitespace:- nothing: the start of the Kotlin code, such as
{<b>x</b>}, - an operator or punctuation character other than
],",'and.:(,,,{,=,:,?,!,&,|,+,-,*,/,%,;,>(as in->),<, ), as inif (vip) <b>VIP</b> else null,- one of the keywords
return,else,in,do,try,throw.
After any other word (a name, a number,
fun,as,is) or after], a string or a character literal,<is left to Kotlin: a comparison or type arguments. - nothing: the start of the Kotlin code, such as
| Code | < means |
|---|---|
tags.map { <li>{it}</li> } |
markup (after {) |
x -> <b/> |
markup (after ->) |
if (vip) <b>VIP</b> else <i>regular</i> |
markup, twice |
a < b, i<n, count <limit |
comparison (a space follows, or a name precedes) |
emptyList<Node>(), fun <T> f(), x as List<Node> |
type arguments |
(a)<b |
markup: write (a) < b |
A markup literal is one element or one fragment. It compiles to xyz.nillpoe.kmx.markup { ... }, whose static type is Node, not Element. Its content is ordinary markup, so it can hold text, expressions, directives and further literals. A literal directly followed by another < is not two literals: wrap several nodes in a fragment.
Directive expressions
The heads of directives are copied into the matching Kotlin statement:
@if (cond) { ... } else if (cond2) { ... } else { ... }
@for (header) { ... } separator { ... } empty { ... }
@when (subject) {
cond -> <tag/>
cond2, cond3 -> { ... }
else -> <other/>
}
@when {
cond -> ...
}
@{ statements }
- A directive starts only with
@if,@foror@whenfollowed directly by(or a space (@whenalso by{), or with@{. Anything else, such ashelp@if.example, is text. A tab or line break after the keyword does not start a directive. @ifandelse iftake anyBooleanexpression. Smart casts from the condition apply inside the block.@for (header)takes any Kotlinforheader:item in items,(index, item) in items.withIndex(),i in 1..3.separator { ... }renders before every item except the first. The item's body runs first and the separator is inserted in front of it, so inside the separator the loop variables hold the item that follows.empty { ... }renders when the loop ran zero times. Loop variables are not in scope there.- Both are optional and may appear in either order, each once.
@whencopies the subject and every branch condition verbatim, sois Type,in range, several values separated by,, andelsework as in Kotlin. Thewhenis a statement: Kotlin requires it to be exhaustive forenum,sealedandBooleansubjects. A branch body is a single tag or fragment, or a{ ... }block. Comments are allowed between branches.@{ ... }copies statements to its position and writes nothing. It can also call the builder directly:@{ text("raw builder call") }.
Names and scope
Declarations
| Declared in | Visible in |
|---|---|
Header code (val total = ..., fun label(x: Int) = ...) |
the whole markup |
@{ val x = ... } |
later siblings in the same element or directive block, and their descendants |
@for (item in items) |
the loop body and separator |
a lambda in an expression (it, named parameters) |
that lambda, including markup literals inside it |
Implicit receivers
A template's render function runs its code inside, from innermost to outermost:
| Receiver | Present in | Members |
|---|---|---|
the current builder (NodeBuilder, or ElementBuilder inside an element) |
markup only, not header code | text, comment, element, insert, + (unary plus), and for elements attribute, attributes |
| the props | props: Type and props(...) templates |
the payload's members ({id}, {items}) |
the render scope (RenderScope) |
templates and components | get, getOrNull, contains, withValue, plus, and extension functions on RenderScope |
Kotlin resolves a name in this order: local declarations (including the parameters props and scope), then each receiver from the innermost, then imports and top-level declarations. Therefore:
- A payload property named like a builder function still works as a property (
{text}readsprops.text), but a payload function with a builder function's name and a matching signature is shadowed. Call it asprops.text(...). - Extension functions on
RenderScoperead like built-ins:{money(total)}callsRenderScope.money. propsandscopealways name the render parameters. They cannot be used asprops(...)parameter names, and a payload member calledscopemust be read asprops.scope.thisis the innermost receiver, usually the builder. Usepropsandscopeinstead ofthis.
Builders carry a @DslMarker: inside a nested element, only the innermost builder is an implicit receiver, so a statement cannot add nodes to an outer element by accident.
Signatures
| Header | props is |
Members read directly |
|---|---|---|
| none | Unit |
none |
props: Order |
the Order passed to render |
Order's members |
props(a: A, b: B = x) |
a generated Props(val a: A, val b: B = x) |
a, b |
component Card(title: String, children: Node) |
not defined; a component is fun RenderScope.Card(title: String, children: Node): Node |
none; title and children are parameters |
Nullability and smart casts
- A nullable value never prints implicitly. In text, only
Node?may be null. In attributes,nullleaves the attribute out. - Smart casts follow Kotlin's rules.
@if (note != null) { <p>{note}</p> }compiles whennoteis stable: a localval, or avalproperty without a custom getter of a class in the same module. For a payload from another module, use{note?.let { <p>{it}</p> }}or copy it into a headerval. - Smart casts reach into nested elements and markup literals, because the builder blocks are inline lambdas.
Evaluation order and control flow
- Header code runs first, then the markup in document order. Within an element, its attributes are evaluated in source order before its children.
- Every expression runs once per render at its position, except inside
@for(once per item, and the separator once per gap, after the body of the item it precedes) and in branches that are not taken. - Builder blocks are inline, so
breakandcontinueinside a@forbody or a@{}block act on the loop, as in Kotlin 2.2 and later. With aseparator,continuein the body skips the item and the separator in front of it. - Do not
returnfrom template code: the render function is an expression that must produce the tree. - Exceptions propagate out of
render. Invalid XML characters in text or attribute values are reported when the tree is written withtoXml, not when it is built.
Whitespace around expressions
Text between tags and expressions follows the JSX rules, applied to each text run separately:
- A run without a line break is kept exactly.
- A run with line breaks is split into lines. Every line but the first loses its leading spaces and tabs; every line but the last loses its trailing ones. Empty lines are dropped, and the rest are joined with one space.
- Inside a directive block, text made only of spaces and tabs between a brace and a tag, an expression or a directive is dropped, as in
{ <b>Paid</b> }. Text next to a brace keeps its spaces:separator {, }writes,.
| Source | Text written |
|---|---|
<p>Hi {name}!</p> |
Hi , name, ! |
<p>\n Hi\n {name}\n</p> |
Hi, name: no space between them |
<p>\n Hi{" "}\n {name}\n</p> |
Hi, , name |
@if (a) { <b/> } |
no text |
Write {" "} for a space the rules remove.
Literal characters and escaping
| To write | In text | In an attribute value |
|---|---|---|
{ } |
{"{"} {"}"} |
{{ }} |
@ |
@ (or @@ where a directive would start) |
@ |
< |
< or {"<"} |
< or < |
& |
&, or a bare & not followed by an entity |
same |
| other characters | & < > " ' © © |
same |
Other named entities, such as ©, are errors. Entities are decoded in source text only; values from expressions are escaped as they are.
On output, text escapes &, <, > and carriage returns; attribute values also escape ", tab and line feed. Markup.trusted(xml) is the only way to write unescaped XML, and it throws if xml is not well-formed.
Errors
The compiler reports syntax errors as file:line:column message. Common ones:
| Message | Cause |
|---|---|
This code is never closed. |
a {, ( or string in Kotlin code has no end, often a line comment that swallowed the } |
Expected `</b>` to close `<b>` from line N. |
mismatched closing tag |
Unexpected `}`. Write `{"}"}` for the character. |
a bare } in text |
`<` starts a tag. Write `<` or `{"<"}` for the character. |
a bare < in text |
Expected `->` after the branch condition. |
a @when branch without -> |
Unknown entity `©`. ... |
an entity other than the six named ones |
`data-x` is not a parameter name. |
a component attribute that is not a Kotlin identifier |
Components take named attributes; ... |
a spread on a component |
`scope` is reserved inside templates; rename the parameter. |
props(scope: ...) or props(props: ...) |
Type errors come from kotlinc and point at the generated file. Every generated statement that holds template code ends with // file.kmx:line, naming the template line. Typical messages:
| kotlinc message | Template cause |
|---|---|
Argument type mismatch: actual type is 'String?', but 'String' was expected. |
a nullable value in {} text, or in an interpolated attribute |
None of the following candidates is applicable for insert or attribute |
a type text or attributes do not accept, such as an enum |
Unresolved reference |
a misspelled name, or a missing import |
No parameter with name 'children' found. |
content inside a component that takes no children |
'when' expression must be exhaustive |
a @when over an enum or sealed type that misses a case |