🎭Expressions

On the previous pages we talked a lot about printable expressions and regular expressions. So let's clarify what exactly an expression is.

An expression is a Go expression that yields a value. Valid Expressions are:

  1. A variable or constant

  2. A Go expression/literal like "abc", 123, or "abc" == "def".

  3. A function call that returns a single value

Additionally, corgi adds a ternary and a "chain" expression. More on that below. And, of course, string interpolation, which you already learned about in the interpolation chapter.

Implementation Note: While you can use `backtick strings` you cannot span them over multiple lines like you can in Go. Depending on your use case there are better alternatives.

Printable Types

If your expression gets printed, e.g. the value of an attribute, you are restricted to these printable types:

  1. uint, uint8, uint16, uint32, uint64

  2. int, int8, int16, int32, int64

  3. string

  4. bool

  5. corgi.HTML, corgi.HTMLAttr, corgi.CSS, corgi.URL, corgi.Srcset, corgi.JS, corgi.JSStr, and corgi.JSAttr in their appropriate contexts (more on them in the Security and Escaping chapter)

  6. Any type implementing fmt.Stringer

  7. A pointer to the above

  8. A (possibly typed) nil which will print nothing

If you try to print something else, the generated function will return with an error.

Ternary Expressions

Aside from the expressions listed above, corgi also supports ternary expressions in the form of a special ternary function.

func ?[T any](cond bool, ifTrue, ifFalse T) T

ifTrue and ifFalse can be any Go expression.

cond can be any Go expression that yields a bool value.

func Ternary(authenticated, admin bool)

p(class = ?(authenticated, "authed", "anon"))
p(class = ?(authenticated, ?(admin, "admin", "authed"), "anon"))
p(class = ?(authenticated && admin, "admin", "not-admin"))
p= ?(admin, "Hello boss!", "Hello someone.")
p Hello, #{?(admin, "boss", "someone")}.
p #{"Hello " + ?(admin, "boss", "someone")}.

Ternary expressions can be used anywhere in a regular Go expression.

Chain Expressions

You know the problem: You want to access data.MyField.MySlice[3], but MyField could be nil, and MySlice might not even be of length 3. The solution is to add ifs that check nilness, length, and map indexes.

However, checks like these create an enormous bloat in template files. This is why corgi adds a second utility that allows you to perform those kind of checks. You can use a ? to perform zero value checks, length/index checks, and checked type assertions for any expression that consists of only accessing fields/maps/slices.

Chain Expressions cannot be embedded inside Go expressions.

// This checks if foo is not zero:
p #{foo?[0]}
// This checks if foo has an index or map entry 0:
p #{foo[0?]}
// This checks if that entry is not zero:
p #{foo[0]?}
// This performs a checked types assertion on that entry:
p #{foo[0].(string)?}
// And this performs all four checks:
p #{foo?[0?]?.(string)?}

You can also add a default if your checks fail by appending a ~. Defaults can be any Go expression.

p #{foo? ~ "some default value"}

Depending on the context you use chain expressions, they behave differently:

  1. When assigning a value to an attribute or setting the value of a mixin argument, that attribute or mixin arg will only be set if the value passes all checks. For required arguments you need to provide a default.

  2. When used in interpolation and a check fails, nothing will be printed.

  3. When used as condition for an if, the if will only be executed if all checks pass.

  4. When used as a range expression in a for loop, that loop will only be executed if the value passes all checks.

Technical Note: Corgi guarantees that it will call each function in a chain expression only once.

If the expression yields a pointer that you want to dereference, you can place an * at the start of the expression to dereference the resolved value. Defaults are assumed to be already dereferenced.

mixin foo(bar string) #{bar}

- s := "foo"
  s1 := &s
foo(bar=*s1? ~ "baz")

Here are a couple more examples, just to make sure you undestand everything:

mixin greet(name="world")
    Hello #{name}!

- var t struct{F *string}

a(href=*t.F?) Click me!
a(href=*t.F? ~ "bar") Click me!

p #{t.F?}

p #+greet(name=*t.F?)
p #+greet(name=*t.F? ~ "universe")

p The value is: #{t.F?}
p The value is #{t.F? ~ "unknown"}

if t.F?
  p The value is: #{t.F}
else
  p The value is not accessible.

Last updated