Repository: CloudyKit/jet (go)

Issue:
Range over slice/map with single var assignment should set the var to the index/key
Currently, `{{ range foo := someSlice }}` will set both `.` and `foo` to the element value of the current iteration. To make this one-var syntax more useful, I would like range to set `foo` to the index of the element instead.

Iterating over a map with a one-variable assignment would set that var to the key of the current element.

Iterating over a channel with a one-variable assignment would set that var to the current element as before, since channel element values don't have a key or index.

`{{ range foo, bar := someSlice }}` would still set `foo` to the index, and `bar`  to the element itself.

This is of course a breaking change, so definitely a major version bump.

Pull request description:
Assign the range key/index in one-var syntax with rangers that provide an index
Basically, if the ranger provides both a range value and index, the one-var syntax sets that var to the index. If the ranger provides no index, one-var syntax will give you the value.

This way, Jet's range now behaves like Go's range for maps, slices, arrays, and channels.

Closes #158.

Patch:
diff --git a/default.go b/default.go
index bcbbf87..c01f89a 100644
--- a/default.go
+++ b/default.go
@@ -17,7 +17,6 @@ package jet
 import (
 	"encoding/json"
 	"errors"
-	"fmt"
 	"html"
 	"io"
 	"io/ioutil"
@@ -39,7 +38,6 @@ func init() {
 		"replace":   reflect.ValueOf(strings.Replace),
 		"split":     reflect.ValueOf(strings.Split),
 		"trimSpace": reflect.ValueOf(strings.TrimSpace),
-		"map":       reflect.ValueOf(newMap),
 		"html":      reflect.ValueOf(html.EscapeString),
 		"url":       reflect.ValueOf(url.QueryEscape),
 		"safeHtml":  reflect.ValueOf(SafeWriter(template.HTMLEscape)),
@@ -48,6 +46,9 @@ func init() {
 		"unsafe":    reflect.ValueOf(SafeWriter(unsafePrinter)),
 		"writeJson": reflect.ValueOf(jsonRenderer),
 		"json":      reflect.ValueOf(json.Marshal),
+		"map":       reflect.ValueOf(newMap),
+		"slice":     reflect.ValueOf(newSlice),
+		"array":     reflect.ValueOf(newSlice),
 		"isset": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
 			a.RequireNumOfArguments("isset", 1, -1)
 			for i := 0; i < len(a.argExpr); i++ {
@@ -72,7 +73,7 @@ func init() {
 				return reflect.ValueOf(expression.NumField())
 			}
 
-			a.Panicf("invalid value type %s in len builtin", expression.Type())
+			a.Panicf("len(): invalid value type %s", expression.Type())
 			return reflect.Value{}
 		})),
 		"includeIfExists": reflect.ValueOf(Func(func(a Arguments) reflect.Value {
@@ -83,7 +84,7 @@ func init() {
 				panic(err)
 			}
 			if err != nil {
-				return hiddenFALSE
+				return hiddenFalse
 			}
 
 			a.runtime.newScope()
@@ -103,7 +104,7 @@ func init() {
 
 			a.runtime.executeList(root)
 
-			return hiddenTRUE
+			return hiddenTrue
 		})),
 		"exec": reflect.ValueOf(Func(func(a Arguments) (result reflect.Value) {
 			a.RequireNumOfArguments("exec", 1, 2)
@@ -151,12 +152,10 @@ func init() {
 
 type hiddenBool bool
 
-func (m hiddenBool) Render(r *Runtime) {
-
-}
+func (m hiddenBool) Render(r *Runtime) { /* render nothing -> hidden */ }
 
-var hiddenTRUE = reflect.ValueOf(hiddenBool(true))
-var hiddenFALSE = reflect.ValueOf(hiddenBool(false))
+var hiddenTrue = reflect.ValueOf(hiddenBool(true))
+var hiddenFalse = reflect.ValueOf(hiddenBool(false))
 
 func jsonRenderer(v interface{}) RendererFunc {
 	return func(r *Runtime) {
@@ -175,14 +174,31 @@ func unsafePrinter(w io.Writer, b []byte) {
 // skipping the escape phase; use this type to create special types of escapee funcs.
 type SafeWriter func(io.Writer, []byte)
 
-func newMap(values ...interface{}) (nmap map[string]interface{}) {
-	if len(values)%2 > 0 {
-		panic("new map: invalid number of arguments on call to map")
+var stringType = reflect.TypeOf("")
+
+var newMap = Func(func(a Arguments) reflect.Value {
+	if a.NumOfArguments()%2 > 0 {
+		panic("map(): incomplete key-value pair (even number of arguments required)")
 	}
-	nmap = make(map[string]interface{})
 
-	for i := 0; i < len(values); i += 2 {
-		nmap[fmt.Sprint(values[i])] = values[i+1]
+	m := reflect.ValueOf(make(map[string]interface{}, a.NumOfArguments()/2))
+
+	for i := 0; i < a.NumOfArguments(); i += 2 {
+		key := a.Get(i)
+		if !key.Type().ConvertibleTo(stringType) {
+			a.Panicf("map(): can't use %+v as string key: %s is not convertible to string", key, key.Type())
+		}
+		key = key.Convert(stringType)
+		m.SetMapIndex(a.Get(i), a.Get(i+1))
 	}
-	return
-}
+
+	return m
+})
+
+var newSlice = Func(func(a Arguments) reflect.Value {
+	arr := make([]interface{}, a.NumOfArguments())
+	for i := 0; i < a.NumOfArguments(); i++ {
+		arr[i] = a.Get(i).Interface()
+	}
+	return reflect.ValueOf(arr)
+})
diff --git a/docs/builtins.md b/docs/builtins.md
new file mode 100644
index 0000000..404dc6c
--- /dev/null
+++ b/docs/builtins.md
@@ -0,0 +1,45 @@
+# Built-ins
+
+- [Functions](#functions)
+  - [From Go](#from-go)
+  - [len](#len)
+  - [isset](#isset)
+  - [exec](#exec)
+  - [ints](#ints)
+
+## Functions
+
+### From Go
+
+The following functions simply expose functions from Go's standard library for convenience:
+
+- `lower`: exposes Go's [strings.ToLower](https://golang.org/pkg/strings/#ToLower)
+- `upper`: exposes Go's [strings.ToUpper](https://golang.org/pkg/strings/#ToUpper)
+- `hasPrefix`: exposes Go's [strings.HasPrefix](https://golang.org/pkg/strings/#HasPrefix)
+- `hasSuffix`: exposes Go's [strings.HasSuffix](https://golang.org/pkg/strings/#HasSuffix)
+- `repeat`: exposes Go's [strings.Repeat](https://golang.org/pkg/strings/#Repeat)
+- `replace`: exposes Go's [strings.Replace](https://golang.org/pkg/strings/#Replace)
+- `split`: exposes Go's [strings.Split](https://golang.org/pkg/strings/#Split)
+- `trimSpace`: exposes Go's [strings.TrimSpace](https://golang.org/pkg/strings/#TrimSpace)
+- `html`: exposes Go's [html.EscapeString](https://golang.org/pkg/html/#EscapeString)
+- `url`: exposes Go's [url.QueryEscape](https://golang.org/pkg/net/url/#QueryEscape)
+
+### len
+
+`len()` takes one argument and returns the length of a string, array, slice or map, the number of fields in a struct, or the buffer size of a channel, depending on the argument's type. (Think of it like Go's `len()` function.)
+
+It panics if you pass a value of any type other than string, array, slice, map, struct or channel.
+
+`len()` indirects through arbitrary layers of pointer and interface types before checking for a valid type.
+
+### isset
+
+`isset()` takes an arbitrary number of index, field, chain or identifier expressions and returns true if all expressions evaluate to non-nil values. It panics only when an unexpected expression type is passed in.
+
+### exec
+
+`exec()` takes a template path and optionally a value to use as context and executes the template with the current or specified context. It returns the last value returned using the `return` statement, or nil if no `return` statement was executed.
+
+### ints
+
+`ints()` takes two integers as lower and upper limit and returns a Ranger producing all the integers between them, including the lower and excluding the upper limit. It panics when the arguments can't be converted to integers or when the upper limit is not strictly greater than the lower limit.
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000..9c60c03
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,4 @@
+# Documentation
+
+- [Syntax Reference](./syntax.md)
+- [Built-ins](./builtins.md)
\ No newline at end of file
diff --git a/docs/syntax.md b/docs/syntax.md
new file mode 100644
index 0000000..1a5a2a1
--- /dev/null
+++ b/docs/syntax.md
@@ -0,0 +1,656 @@
+# Syntax Reference
+
+- [Comments](#comments)
+- [Variables](#variables)
+  - [Initialization](#initialization)
+  - [Assignment](#assignment)
+- [Expressions](#expressions)
+  - [Identifiers](#identifiers)
+  - [Indexing](#indexing)
+    - [String](#string)
+    - [Slice / Array](#slice--array)
+    - [Map](#map)
+    - [Struct](#struct)
+  - [Field access](#field-access)
+    - [Map](#map-1)
+    - [Struct](#struct-1)
+  - [Slicing](#slicing)
+  - [Arithmetic](#arithmetic)
+  - [String concatenation](#string-concatenation)
+    - [Logical operators](#logical-operators)
+  - [Ternary operator](#ternary-operator)
+  - [Method calls](#method-calls)
+  - [Function calls](#function-calls)
+    - [Prefix syntax](#prefix-syntax)
+    - [Pipelining](#pipelining)
+    - [Combining syntaxes](#combining-syntaxes)
+- [Control Structures](#control-structures)
+  - [if](#if)
+    - [if / else](#if--else)
+    - [if / else if](#if--else-if)
+    - [if / else if / else](#if--else-if--else)
+  - [range](#range)
+    - [Slices / Arrays](#slices--arrays)
+    - [Maps](#maps)
+    - [Channels](#channels)
+    - [else](#else)
+  - [try](#try)
+  - [try / catch](#try--catch)
+- [Templates](#templates)
+  - [include](#include)
+  - [return](#return)
+- [Blocks](#blocks)
+  - [block](#block)
+  - [yield](#yield)
+  - [content](#content)
+  - [Recursion](#recursion)
+  - [extends](#extends)
+  - [import](#import)
+
+## Comments
+
+Comments begin with `{*` and end with `*}` and will simply be dropped during template parsing.
+
+    {* this is a comment *}
+
+Comments can span multiple lines:
+
+    {*
+        none of this will be executed:
+        {{ asd }}
+        {{ include "./foo.jet" }}
+    *}
+
+## Variables
+
+### Initialization
+
+Variables have to be initialised before they can be used:
+
+    {{ foo := "bar" }}
+
+### Assignment
+
+Initialised variables can be assigned a new value:
+
+    {{ foo = "asd" }}
+
+Variables initialised inside a template have no fixed type, so this is valid, too:
+
+    {{ foo = 4711 }}
+
+
+## Expressions
+
+### Identifiers
+
+Function and variable names are identifiers. Identifiers simply evaluate to the value stored for them in a variable scope, the globals, or the default variables. For example, the following are identifiers that resolve to built-in functions:
+
+- `len`
+- `isset`
+- `split`
+
+After `{{ foo := "foo" }}`, the `foo` in `{{ len(foo) }}` is an identifier expression and resolved to the string "foo".
+
+### Indexing
+
+Indexing expressions use `[]` syntax and evaluate to a byte in a string, an element in a slice or array, a value in a map, or a field of a struct.
+
+#### String
+
+    {{ s := "helloworld" }}
+    {{ s[1] }} <!-- renders 101, the ASCII value of e -->
+
+#### Slice / Array
+
+    {{ s := slice("foo", "bar", "asd") }}
+    {{ s[0] }} <!-- renders foo -->
+    {{ i := 2 }}
+    {{ s[i] }} <!-- renders asd -->
+
+#### Map
+
+    {{ m := map("foo", 123, "bar", 456) }}
+    {{ m["foo"] }} <!-- renders 123 -->
+    {{ bar := "bar" }}
+    {{ m[bar] }} <!-- renders 456 -->
+
+#### Struct
+
+Assuming `user` is a Go struct value with a string field "Name":
+
+    {{ user["Name"] }}
+
+### Field access
+
+Field access expressions use dot notation (`foo.bar`) and can be used with maps or structs. When the identifier in front of the `.` is omitted, the field is looked up in the current context (which will fail if the context is not a map or struct).
+
+#### Map
+
+    {{ m := map("foo", 123, "bar", 456) }}
+    {{ m.foo }} <!-- renders 123 -->
+    {{ s := slice(m, map("foo", 4711)) }}
+    {{ range s }}
+        {{ .foo }} <!-- renders 123, then 4711 -->
+    {{ end }}
+
+#### Struct
+
+Assuming `user` is a Go struct value with a string field "Name":
+
+    {{ user.Name }}
+
+Assuming `users` is a slice of Go structs, o:
+
+    {{ range users }}
+        {{ .Name }}
+    {{ end }}
+
+### Slicing
+
+You may re-slice a slice or array using the Go-like [start:end] syntax. The element at the `start` index will be included, the one at the `end` index will be excluded.
+
+    {{ s := slice(6, 7, 8, 9, 10, 11) }}
+    {{ sevenEightNine := s[1:4] }}
+
+### Arithmetic
+
+Basic arithmetic operators are supported: `+`, `-`, `*`, `/`, `%`
+
+    {{ 1 + 2 * 3 - 4 }} <!-- will print 3 (1+6-4) -->
+    {{ (1 + 2) * 3 - 4.1 }} <!-- will print 4.9 -->
+
+### String concatenation
+
+    {{ "HELLO" + " " + "WORLD!" }} <!-- will print "HELLO WORLD!" -->
+
+#### Logical operators
+
+The following operators are supported:
+
+- `&&`: and
+- `||`: or
+- `!`: not
+- `==`: equal
+- `!=`: not equal
+- `>`: greater than
+- `>=`: greater than or equal (= not less than)
+- `<`: less than
+- `<=`: less than or equal (= not greater than)
+
+Examples:
+
+    {{ item == true || !item2 && item3 != "test" }}
+
+    {{ item >= 12.5 || item < 6 }}
+
+Logical expressions always evaluate to either `true` or `false`.
+
+### Ternary operator
+
+` x ? y : z` evaluates to `y` if `x` is truthy or `z` otherwise.
+
+    <title>{{ .HasTitle ? .Title : "Title not set" }}</title>
+
+### Method calls
+
+You can call exported methods of Go types:
+
+    {{ user.Rename("Peter") }}
+    {{ range users }}
+        {{ .FullName() }}
+    {{ end }}
+
+### Function calls
+
+Function calls can be written using familiar syntax:
+
+    {{ length := len(s) }}
+
+#### Prefix syntax
+
+Function calls can also be written using a colon instead of parentheses:
+
+    {{ isset: foo, bar }}
+
+Note that function calls using this syntax can't be nested! This is valid: `{{ len: slice("asd", "foo") }}`, but this isn't: `{{ len: slice: "asd", "foo" }}`
+
+#### Pipelining
+
+Pipelining works by "piping" a value into a function as its first argument:
+
+    {{ "123" | len}}
+    {{ "FOO" | lower | len }}
+
+Pipelines are evaluated left-to-right. This chaining syntax may be easier to read than deeply nested calls:
+
+    {{ "123" | lower | upper | len }}
+
+is equivalent to
+
+    {{ len(upper(lower("123"))) }}
+
+#### Combining syntaxes
+
+Inside a pipeline, functions can be enriched with additional parameters using prefix syntax, but not using parentheses. This is ok:
+
+    {{ "hello" | repeat: 2 | len }}
+
+And equivalent to this:
+
+    {{ len(repeat("hello", 2)) }}
+
+The following is **invalid**:
+
+    {{ "hello" | slice("world") | len }}
+
+A call expression using parentheses in a pipeline is evaluated before before it's looked at in the context of the pipeline, meaning function calls using `()` after a `|` must evaluate to a function.
+
+`slice("world"))` is evaluated before it's used in the pipeline (to a slice value), causing the piping to fail (because slices can't be called like functions).
+
+However, assuming you have a custom function `translateTo()` that returns either the `translateToSpanish()` or the `translateToJapanese()` function, both of these are valid and do the same thing:
+
+    {{ "an english term" | translateToSpanish }}
+    {{ "an english term" | translateTo("spanish") }}
+
+Please note that the raw, unsafe, safeHtml or safeJs built-in escapers or custom safe writers need to be the last command evaluated in an action node. This means they have to come last when used in a pipeline.
+
+    {{ "hello" | upper | raw }} <!-- valid -->
+    {{ raw: "hello" }}          <!-- valid -->
+    {{ raw: "hello" | upper }}  <!-- invalid (upper would be evaluated after raw) -->
+
+## Control Structures
+
+### if
+
+You can branch inside templates depending on a condition using `if`:
+
+    {{ if foo == "asd" }}
+        foo is 'asd'!
+    {{ end }}
+
+#### if / else
+
+You may provide an `else` block when using `if`:
+
+    {{ if foo == "asd" }}
+        foo is 'asd'!
+    {{ else }}
+        foo is something else!
+    {{ end }}
+
+#### if / else if
+
+You can test for another condition using `else if`:
+
+    {{ if foo == "asd" }}
+        foo is 'asd'!
+    {{ else if foo == 4711 }}
+        foo is 4711!
+    {{ end }}
+
+This is exactly the same as this code:
+
+    {{ if foo == "asd" }}
+        foo is 'asd'!
+    {{ else }}
+        {{ if foo == 4711 }}
+            foo is 4711!
+        {{ end }}
+    {{ end }}
+
+
+#### if / else if / else
+
+`if / else if / else` works, too, of course:
+
+    {{ if foo == "asd" }}
+        foo is 'asd'!
+    {{ else if foo == 4711 }}
+        foo is 4711!
+    {{ else }}
+        foo is something else!
+    {{ end }}
+
+and will do exactly the same as this:
+
+    {{ if foo == "asd" }}
+        foo is 'asd'!
+    {{ else }}
+        {{ if foo == 4711 }}
+            foo is 4711!
+        {{ else }}
+            foo is something else!
+        {{ end }}
+    {{ end }}
+
+### range
+
+Use `range` to iterate over data, just like you would in Go, or how you would use a `foreach` loop in other programming languages. Inside the `range`, the context (`.`) is set to the current iteration's value:
+
+    {{ s := slice("foo", "bar", "asd") }}
+    {{ range s }}
+        {{.}}
+    {{ end }}
+
+Jet provides built-in rangers for Go slices, arrays, maps, and channels. You can add your own by implementing the Ranger interface. TODO
+
+#### Slices / Arrays
+
+When iterating over a slice or array, Jet can give you the current iteration index:
+
+    {{ range i := s }}
+        {{i}}: {{.}}
+    {{ end }}
+
+If you want, you can have Jet assign the iteration value to another value:
+
+    {{ range i, v := s }}
+        {{i}}: {{v}}
+    {{ end }}
+
+The iteration value will then not be used as context (`.`); instead, the parent context remains available.
+
+#### Maps
+
+When iterating over a map, Jet can give you the key current iteration index:
+
+    {{ m := map("foo", "bar", "asd", 123)}}
+    {{ range k := m }}
+        {{k}}: {{.}}
+    {{ end }}
+
+Just like with slices, you can have Jet assign the iteration value to another value:
+
+    {{ range k, v := m }}
+        {{k}}: {{v}}
+    {{ end }}
+
+The iteration value will then not be used as context (`.`); instead, the parent context remains available.
+
+#### Channels
+
+When iterating over a channel, you can can have Jet assign the iteration value to another value in order to keep the parent context, similar to the two-variables syntax for slices and maps:
+
+    {{ range v := c }}
+        {{v}}
+    {{ end }}
+
+It's an error to use channels together with the two-variable syntax.
+
+#### else
+
+`range` statements can have an `else` block which is executed if there are non values to range over (as signalled by the Ranger). For example, it will run when iterating an empty slice, array or map or a closed channel:
+
+    {{ range searchResults }}
+        {{.}}
+    {{ else }}
+        No results found :(
+    {{ end }}
+
+### try
+
+If you want to attempt rendering something, but don't want Jet to crash when something goes wrong, you can use `try`:
+
+    {{ try }}
+        we're not sure if we already initialised foo,
+        so the next line might fail...
+        {{ foo }}
+    {{ end }}
+
+You can do anything you want inside a `try` block, even yield blocks or include other templates.
+
+All render output generated inside the `try` block is buffered and only included in the surrounding output after execution of the entire block completed successfully. Any runtime error means no content from inside `try` is kept.
+
+### try / catch
+
+In case of an error inside the `try` block, you can have Jet evaluate a `catch` block:
+
+    {{ try }}
+        we're not sure if we already initialised foo,
+        so the next line might fail...
+        {{ foo }}
+    {{ catch }}
+        foo was not initialised, this is fallback content
+    {{ end }}
+
+Errors occuring inside the `catch` block are not caught and will cause execution to abort.
+
+You can also have the error that occured assigned to a variable inside the `catch` block to log it or otherwise handle it. Since it's a Go error value, you have to call `.Error()` on it to get the error message as a string.
+
+    {{ try }}
+        we're not sure if we already initialised foo,
+        so the next line might fail...
+        {{ foo }}
+    {{ catch err }}
+        {{ log(err.Error()) }}
+        uh oh, something went wrong: {{ err.Error() }}
+    {{ end }}
+
+`err` will not be available outside the `catch` block.
+
+## Templates
+
+### include
+
+Including a template is similar to using partials in other template languages. All local and global variables are available to you in the included template. You can pass a context by specifying it as the last argument in the `include` statement. If you don't pass a context, the current context will be kept.
+
+    <!-- file: "user.jet" -->
+    <div class="user">
+        {{ .["name"] }}: {{ .["email"] }}
+    </div>
+
+    <!-- file: "index.jet" -->
+    {{ range users }}
+        {{ include "./user.jet" }}
+    {{ end }}
+
+Executing `index.jet` with
+
+    {{ users := map(
+        "4243", map("name", "Peter", "email", "peter@aol.com"),
+        "4534", map("name", "Bob", "email", "bob@yahoo.com")
+    ) }}
+
+gives you:
+
+    <div class="user">
+        Peter: peter@aol.com
+    </div>
+    <div class="user">
+        Bob: bob@yahoo.com
+    </div>
+
+### return
+
+Templates can set a value as their return value using `return`. This is only useful when the template was executed using the `exec()` built-in function, which will make the return value of a template available in another template.
+
+    <!-- file: "foo.jet" -->
+    {{ f := "f" }}
+    {{ o := "o" }}
+    {{ return f+o+o }}
+
+    <!-- file: "bar.jet" -->
+    {{ foo := exec("./foo.jet") }}
+    Hello, {{ foo }}!
+
+The output will simply be:
+
+    Hello, foo!
+
+
+## Blocks
+
+You can think of blocks as partials or pieces of a template that you can invoke by name.
+
+### block
+
+To define a block, use `block`:
+
+    {{block copyright()}}
+        <div>© ACME, Inc. 2020</div>
+    {{end}}
+
+Defining a block in a template that's being executed will also invoke it immediately. To avoid this, use `import` or `extends`. Blocks can't be named "content", "yield", or other Jet keywords.
+
+A block definition accepts a comma-separated list of argument names, with optional defaults:
+
+    {{ block inputField(type="text", label, id, value="", required=false) }}
+        <div class="form-field">
+            <label for="{{ id }}">{{ label }}</label>
+            <input type="{{ type }}" value="{{ value }}" id="{{ id }}" {{ required ? "required" : "" }} />
+        </div>
+    {{ end }}
+
+### yield
+
+To invoke a previously defined block, use `yield`:
+
+    <footer>
+        {{yield copyright()}}
+    </footer>
+
+    {{yield inputField(id="firstname", label="First name", required=true)}}
+
+The sequence of parameters is irrelevant, and parameters without a default value must be passed when yielding a block.
+
+You can pass something to be used as context, or the current context will be passed. Given
+
+    {{block buff()}}
+        <strong>{{.}}</strong>
+    {{end}}
+
+the following invocation
+
+    {{yield buff() "Batman"}}
+
+will produce
+
+    <strong>Batman</Batman>
+
+### content
+
+When defining a block, use the special `{{ yield content }}` statement to designate where any inner content should be rendered. Then, when you invoke the block with yield, use the keyword content at the end of the `yield`. For example:
+
+    {{ block link(target) }}
+        <a href="{{ target }}">{{ yield content }}</a>
+    {{ end }}
+
+    [...]
+
+    {{ yield link(target="https://www.example.com") content }}
+        Example Inc.
+    {{ end }}
+
+The output will be
+
+    <a href="https://www.example.com">Example Inc.</a>
+
+The invocating `yield` (`{{ yield link(target="https://www.example.com") content }}`) will store the content (together with the current variable scope) and the `{{ yield content }}` part will restore the variable scope and execute the content. When you pass a context during the `yield` block invocation, it will be used when executing the content as well.
+
+Since defining a block will also invoke it, you can also define some content immediately as part of the `block` definition:
+
+    {{ name := "Sarah" }}
+    {{ block header() }}
+        <div class="header">
+        {{ yield content }}
+        </div>
+    {{ content }}
+        <h1>Hey {{ name }}!</h1>
+    {{ end }}
+
+This will render something like the following at the position where the block is defined:
+
+    <div class="header">
+        <h1>Hey Sarah!</h1>
+    </div>
+
+### Recursion
+
+You can yield a block inside its own definition:
+
+    {{ block menu() }}
+        <ul>
+            {{ range . }}
+                <li>{{ .Text }}{{ if len(.Children) }}{{ yield menu() .Children }}{{ end }}</li>
+            {{ end }}
+        </ul>
+    {{ end }}
+
+### extends
+
+A template can extend another template using an `extends` statement followed by a template path at the very top:
+
+    <!-- file: "content.jet" -->
+    {{extends "./layout.jet"}}
+
+In an extending template, content outside of a block definition will be discarded:
+
+    <!-- file: "content.jet" -->
+    {{extends "./layout.jet"}}
+    {{block body()}}
+    <main>
+        This content can be yielded anywhere.
+    </main>
+    {{end}}
+    This content will never be rendered.
+
+The extended template then has to have yield slots to render your blocks into:
+
+    <!-- file: "layout.jet" -->
+    <!DOCTYPE html>
+    <html>
+    <body>
+        {{yield body()}}
+    </body>
+    </html>
+
+The final result will be:
+
+    <!DOCTYPE html>
+    <html>
+    <body>
+        <main>
+            This content can be yielded anywhere.
+        </main>
+    </body>
+    </html>
+
+Every template can only extend one other template, and the `extends` statement has to be at the very top of the file (even above `import` statements).
+
+Since the extending template isn't actually executed (the extended template is), the blocks defined in it don't run until you `yield` them explicitely.
+
+### import
+
+A template's defined blocks can be imported into another template using the `import` statement:
+
+    <!-- file: "my_blocks.jet" -->
+    {{ block body() }}
+    <main>
+        This content can be yielded anywhere.
+    </main>
+    {{ end }}
+
+    <!-- file: "index.jet" -->
+    {{ import "./my_blocks.jet" }}
+    <!DOCTYPE html>
+    <html>
+    <body>
+        {{ yield body() }}
+    </body>
+    </html>
+
+Executing `index.jet` will produce:
+
+    <!DOCTYPE html>
+    <html>
+    <body>
+        <main>
+            This content can be yielded anywhere.
+        </main>
+    </body>
+    </html>
+
+`import` makes all the blocks from the imported template available in the importing template. There is no way to only import (a) specific block(s).
+
+Since the imported template isn't actually executed, the blocks defined in it don't run until you `yield` them explicitely.
\ No newline at end of file
diff --git a/eval.go b/eval.go
index 6f65a96..e8225b7 100644
--- a/eval.go
+++ b/eval.go
@@ -16,6 +16,7 @@ package jet
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"io"
 	"reflect"
@@ -447,12 +448,15 @@ func (st *Runtime) executeList(list *ListNode) (returnValue reflect.Value) {
 
 			isSet := node.Set != nil
 			isLet := false
-			isKeyVal := false
+			keyVarSlot := 0
+			valVarSlot := -1
 
 			context := st.context
 
 			if isSet {
-				isKeyVal = len(node.Set.Left) > 1
+				if len(node.Set.Left) > 1 {
+					valVarSlot = 1
+				}
 				expression = st.evalPrimaryExpressionGroup(node.Set.Right[0])
 				if node.Set.Let {
 					isLet = true
@@ -463,26 +467,35 @@ func (st *Runtime) executeList(list *ListNode) (returnValue reflect.Value) {
 			}
 
 			ranger, cleanup := getRanger(expression)
+			if !ranger.ProvidesIndex() {
+				if len(node.Set.Left) > 1 {
+					// two-vars assignment with ranger that doesn't provide an index
+					node.error(errors.New("two-var range over ranger that does not provide an index"))
+				}
+				keyVarSlot, valVarSlot = -1, 0
+			}
+
 			indexValue, rangeValue, end := ranger.Range()
 			if !end {
 				for !end && !returnValue.IsValid() {
 					if isSet {
 						if isLet {
-							if isKeyVal {
-								st.variables[node.Set.Left[0].String()] = indexValue
-								st.variables[node.Set.Left[1].String()] = rangeValue
-							} else {
-								st.variables[node.Set.Left[0].String()] = rangeValue
+							if keyVarSlot >= 0 {
+								st.variables[node.Set.Left[keyVarSlot].String()] = indexValue
+							}
+							if valVarSlot >= 0 {
+								st.variables[node.Set.Left[valVarSlot].String()] = rangeValue
 							}
 						} else {
-							if isKeyVal {
-								st.executeSet(node.Set.Left[0], indexValue)
-								st.executeSet(node.Set.Left[1], rangeValue)
-							} else {
-								st.executeSet(node.Set.Left[0], rangeValue)
+							if keyVarSlot >= 0 {
+								st.executeSet(node.Set.Left[keyVarSlot], indexValue)
+							}
+							if valVarSlot >= 0 {
+								st.executeSet(node.Set.Left[valVarSlot], rangeValue)
 							}
 						}
-					} else {
+					}
+					if valVarSlot < 0 {
 						st.context = rangeValue
 					}
 					returnValue = st.executeList(node.List)
@@ -1500,9 +1513,10 @@ func resolveIndex(v, index reflect.Value) (reflect.Value, error) {
 		return reflect.Value{}, fmt.Errorf("can't use %s as field name in struct type %s", index, v.Type())
 	case reflect.Map:
 		// If it's a map, attempt to use the field name as a key.
-		if !index.Type().AssignableTo(v.Type().Key()) {
+		if !index.Type().ConvertibleTo(v.Type().Key()) {
 			return reflect.Value{}, fmt.Errorf("can't use %s (%s) as key for map of type %s", index, index.Type(), v.Type())
 		}
+		index = index.Convert(v.Type().Key()) // noop in most cases, but not expensive
 		return indirectEface(v.MapIndex(index)), nil
 	case reflect.Ptr:
 		etyp := v.Type().Elem()
diff --git a/ranger.go b/ranger.go
index df03c7c..c4f9736 100644
--- a/ranger.go
+++ b/ranger.go
@@ -11,6 +11,7 @@ import (
 // encountered on the right hand side of a range's "let" expression.
 type Ranger interface {
 	Range() (reflect.Value, reflect.Value, bool)
+	ProvidesIndex() bool
 }
 
 type intsRanger struct {
@@ -27,6 +28,8 @@ func (r *intsRanger) Range() (index, value reflect.Value, end bool) {
 	return
 }
 
+func (r *intsRanger) ProvidesIndex() bool { return true }
+
 type pooledRanger interface {
 	Ranger
 	Setup(reflect.Value)
@@ -56,22 +59,7 @@ func (r *sliceRanger) Range() (index, value reflect.Value, end bool) {
 	return
 }
 
-type chanRanger struct {
-	v reflect.Value
-}
-
-var _ Ranger = &chanRanger{}
-var _ pooledRanger = &chanRanger{}
-
-func (r *chanRanger) Setup(v reflect.Value) {
-	r.v = v
-}
-
-func (r *chanRanger) Range() (_, value reflect.Value, end bool) {
-	v, ok := r.v.Recv()
-	value, end = v, !ok
-	return
-}
+func (r *sliceRanger) ProvidesIndex() bool { return true }
 
 type mapRanger struct {
 	iter    *reflect.MapIter
@@ -96,6 +84,27 @@ func (r *mapRanger) Range() (key, value reflect.Value, end bool) {
 	return
 }
 
+func (r *mapRanger) ProvidesIndex() bool { return true }
+
+type chanRanger struct {
+	v reflect.Value
+}
+
+var _ Ranger = &chanRanger{}
+var _ pooledRanger = &chanRanger{}
+
+func (r *chanRanger) Setup(v reflect.Value) {
+	r.v = v
+}
+
+func (r *chanRanger) Range() (_, value reflect.Value, end bool) {
+	v, ok := r.v.Recv()
+	value, end = v, !ok
+	return
+}
+
+func (r *chanRanger) ProvidesIndex() bool { return false }
+
 // ranger pooling
 
 var (
diff --git a/template.go b/template.go
index 31571f3..c77ab1c 100644
--- a/template.go
+++ b/template.go
@@ -265,13 +265,14 @@ func (t *Template) String() (template string) {
 }
 
 func (t *Template) addBlocks(blocks map[string]*BlockNode) {
-	if len(blocks) > 0 {
-		if t.processedBlocks == nil {
-			t.processedBlocks = make(map[string]*BlockNode)
-		}
-		for key, value := range blocks {
-			t.processedBlocks[key] = value
-		}
+	if len(blocks) == 0 {
+		return
+	}
+	if t.processedBlocks == nil {
+		t.processedBlocks = make(map[string]*BlockNode)
+	}
+	for key, value := range blocks {
+		t.processedBlocks[key] = value
 	}
 }
 


Test patch:
diff --git a/eval_test.go b/eval_test.go
index 9527d91..874a357 100644
--- a/eval_test.go
+++ b/eval_test.go
@@ -215,7 +215,7 @@ func TestEvalIfNode(t *testing.T) {
 	RunJetTest(t, data, nil, "ifNode_else", `{{if false}}hello{{else}}world{{end}}`, `world`)
 	RunJetTest(t, data, nil, "ifNode_elseif", `{{if false}}hello{{else if true}}world{{end}}`, `world`)
 	RunJetTest(t, data, nil, "ifNode_elseif_else", `{{if false}}hello{{else if false}}world{{else}}buddy{{end}}`, `buddy`)
-	RunJetTest(t, data, nil, "ifNode_string_comparison", `{{user.Name}} (email: {{user.Email}}): {{if user.Email == "email2@example.com"}}email is email2@example.com{{else}}email is not email2@example.com{{end}}`, `José Santos (email: email@example.com): email is not email2@example.com`)
+	RunJetTest(t, data, nil, "ifNode_string_comparison", `{{user["Name"]}} (email: {{user.Email}}): {{if user.Email == "email2@example.com"}}email is email2@example.com{{else}}email is not email2@example.com{{end}}`, `José Santos (email: email@example.com): email is not email2@example.com`)
 
 }
 
@@ -289,7 +289,7 @@ func TestEvalRangeNode(t *testing.T) {
 
 	const resultString = `<h1>Mario Santos<small>mario@gmail.com</small></h1><h1>Joel Silva<small>joelsilva@gmail.com</small></h1><h1>Luis Santana<small>luis.santana@gmail.com</small></h1>`
 	RunJetTest(t, data, nil, "Range_Expression", `{{range users}}<h1>{{.Name}}<small>{{.Email}}</small></h1>{{end}}`, resultString)
-	RunJetTest(t, data, nil, "Range_ExpressionValue", `{{range user:=users}}<h1>{{user.Name}}<small>{{user.Email}}</small></h1>{{end}}`, resultString)
+	RunJetTest(t, data, nil, "Range_ExpressionValue", `{{range _,user:=users}}<h1>{{user.Name}}<small>{{user.Email}}</small></h1>{{end}}`, resultString)
 	var resultString2 = `<h1>0: Mario Santos<small>mario@gmail.com</small></h1><h1>Joel Silva<small>joelsilva@gmail.com</small></h1><h1>2: Luis Santana<small>luis.santana@gmail.com</small></h1>`
 	RunJetTest(t, data, nil, "Range_ExpressionValueIf", `{{range i, user:=users}}<h1>{{if i == 0 || i == 2}}{{i}}: {{end}}{{user.Name}}<small>{{user.Email}}</small></h1>{{end}}`, resultString2)
 }
@@ -547,6 +547,7 @@ func TestEvalBuiltinExpression(t *testing.T) {
 			},
 		},
 	)
+	RunJetTest(t, data, nil, "LenExpression_pipeline", `{{"123" | len}}`, "3")
 	RunJetTest(t, data, nil, "IsSetExpression_7", `{{isset(foo)}}`, "true")
 	RunJetTest(t, data, nil, "IsSetExpression_8", `{{isset(foo.asd)}}`, "true")
 	RunJetTest(t, data, nil, "IsSetExpression_9", `{{isset(foo.asd.bar)}}`, "true")
@@ -558,6 +559,8 @@ func TestEvalBuiltinExpression(t *testing.T) {
 		"foo", map[string]interface{}{},
 	)
 	RunJetTest(t, data, nil, "IsSetExpression_14", `{{isset(foo.asd[0].bar)}}`, "false")
+	RunJetTest(t, data, nil, "IsSetExpression_pipeline", `{{foo | isset}}`, "true")
+
 }
 
 func TestEvalAutoescape(t *testing.T) {
@@ -643,6 +646,13 @@ func TestTryCatch(t *testing.T) {
 	RunJetTestWithSet(t, set, nil, nil, "try_include", "", "before broken include ...\n\nafter broken include ...")
 }
 
+func TestBuiltinCollectionFuncs(t *testing.T) {
+	RunJetTest(t, nil, nil, "map_builtin", `{{ m := map( "foo", "bar", "asd", 123)}}{{m}}`, "map[asd:123 foo:bar]")
+	RunJetTest(t, nil, nil, "slice_builtin", `{{ m := slice( "foo", "bar", "asd", 123)}}{{m}}`, "[foo bar asd 123]")
+	RunJetTest(t, nil, nil, "slice_builtin_subslice", `{{ m := slice( "foo", "bar", "asd", 123)}}{{m[1:3]}}`, "[bar asd]")
+	RunJetTest(t, nil, nil, "array_builtin", `{{ m := array( "foo", "bar", "asd", 123)}}{{m}}`, "[foo bar asd 123]")
+}
+
 func TestRanger(t *testing.T) {
 	c := make(chan string)
 	go func() {
@@ -654,15 +664,20 @@ func TestRanger(t *testing.T) {
 	var data = make(VarMap)
 	data.Set(
 		"m", map[string]interface{}{
-			"asd": 1,
+			"asd": 123,
 			//"foo": "blub", // adding more values here means we can't predict the order below
 			//"bar": nil,
 		},
 	)
 	data.Set("s", []string{"asd", "foo", "bar"})
 	data.Set("c", c)
-	RunJetTest(t, data, nil, "slice_ranger", `{{ range i, v := s }}{{i}}:{{v}},{{ end }}`, "0:asd,1:foo,2:bar,")
-	RunJetTest(t, data, nil, "map_ranger", `{{ range k, v := m }}{{k}}:{{v}},{{ end }}`, "asd:1,")
+	RunJetTest(t, data, nil, "slice_ranger", `{{ range s }}{{.}},{{ end }}`, "asd,foo,bar,")
+	RunJetTest(t, data, nil, "slice_ranger_value", `{{ range v := s }}{{v}},{{ end }}`, "0,1,2,")
+	RunJetTest(t, data, nil, "slice_ranger_value_context", `{{ range v := s }}{{.}},{{ end }}`, "asd,foo,bar,")
+	RunJetTest(t, data, nil, "slice_ranger_index_value", `{{ range i, v := s }}{{i}}:{{v}},{{ end }}`, "0:asd,1:foo,2:bar,")
+	RunJetTest(t, data, nil, "map_ranger", `{{ range m }}{{.}},{{ end }}`, "123,")
+	RunJetTest(t, data, nil, "map_ranger_key_context", `{{ range k := m }}{{k}}:{{.}},{{ end }}`, "asd:123,")
+	RunJetTest(t, data, nil, "map_ranger_key_value", `{{ range k, v := m }}{{k}}:{{v}},{{ end }}`, "asd:123,")
 	RunJetTest(t, data, nil, "chan_ranger", `{{ range v := c }}{{v}}{{ end }}`, "0123456789")
 	RunJetTest(t, nil, nil, "ints_ranger", `{{ range i := ints(0, 10) }}{{ (i == 0 ? "" : ", ") + i }}{{ end }}`, "0, 1, 2, 3, 4, 5, 6, 7, 8, 9")
 }