[gnome-builder/wip/chergert/templates] wip on templating ideas



commit 6f269618d0bda9d719fc524cddb10d2c7327869d
Author: Christian Hergert <christian hergert me>
Date:   Wed Jan 6 17:32:22 2016 -0800

    wip on templating ideas

 configure.ac                           |    3 +
 contrib/Makefile.am                    |    1 +
 contrib/tmpl/Makefile.am               |   52 ++++
 contrib/tmpl/configure.ac.tmpl         |  203 +++++++++++++++
 contrib/tmpl/tmpl-branch-node.c        |  221 +++++++++++++++++
 contrib/tmpl/tmpl-branch-node.h        |   38 +++
 contrib/tmpl/tmpl-condition-node.c     |  155 ++++++++++++
 contrib/tmpl/tmpl-condition-node.h     |   36 +++
 contrib/tmpl/tmpl-error.c              |   25 ++
 contrib/tmpl/tmpl-error.h              |   40 +++
 contrib/tmpl/tmpl-expr-node.c          |   97 +++++++
 contrib/tmpl/tmpl-expr-node.h          |   36 +++
 contrib/tmpl/tmpl-expr-parse.c         |   34 +++
 contrib/tmpl/tmpl-expr-private.h       |   33 +++
 contrib/tmpl/tmpl-expr.c               |  123 +++++++++
 contrib/tmpl/tmpl-expr.h               |   42 +++
 contrib/tmpl/tmpl-iter-node.c          |  158 ++++++++++++
 contrib/tmpl/tmpl-iter-node.h          |   38 +++
 contrib/tmpl/tmpl-iterator.c           |  142 +++++++++++
 contrib/tmpl/tmpl-iterator.h           |   50 ++++
 contrib/tmpl/tmpl-lexer.c              |  183 ++++++++++++++
 contrib/tmpl/tmpl-lexer.h              |   49 ++++
 contrib/tmpl/tmpl-node.c               |  298 ++++++++++++++++++++++
 contrib/tmpl/tmpl-node.h               |   65 +++++
 contrib/tmpl/tmpl-parser.c             |  267 ++++++++++++++++++++
 contrib/tmpl/tmpl-parser.h             |   48 ++++
 contrib/tmpl/tmpl-scope.c              |  102 ++++++++
 contrib/tmpl/tmpl-scope.h              |   38 +++
 contrib/tmpl/tmpl-template-locator.c   |  197 +++++++++++++++
 contrib/tmpl/tmpl-template-locator.h   |   57 +++++
 contrib/tmpl/tmpl-template.c           |  427 ++++++++++++++++++++++++++++++++
 contrib/tmpl/tmpl-template.h           |   66 +++++
 contrib/tmpl/tmpl-text-node.c          |   90 +++++++
 contrib/tmpl/tmpl-text-node.h          |   37 +++
 contrib/tmpl/tmpl-token-input-stream.c |  307 +++++++++++++++++++++++
 contrib/tmpl/tmpl-token-input-stream.h |   43 ++++
 contrib/tmpl/tmpl-token.c              |  170 +++++++++++++
 contrib/tmpl/tmpl-token.h              |   54 ++++
 contrib/tmpl/tmpl.c                    |   74 ++++++
 contrib/tmpl/tmpl.h                    |   35 +++
 40 files changed, 4134 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 53b5a39..8d50780 100644
--- a/configure.ac
+++ b/configure.ac
@@ -181,6 +181,8 @@ PKG_CHECK_MODULES(PYGOBJECT,[pygobject-3.0 >= pygobject_required_version],
                             [have_pygobject=no])
 PKG_CHECK_MODULES(RG,       [gtk+-3.0 >= gtk_required_version])
 PKG_CHECK_MODULES(SEARCH,   [glib-2.0 >= glib_required_version])
+PKG_CHECK_MODULES(TMPL,     [gio-2.0 >= glib_required_version
+                             gio-unix-2.0 >= glib_required_version])
 PKG_CHECK_MODULES(XML,      [gio-2.0 >= glib_required_version
                              libxml-2.0 >= libxml_required_version])
 
@@ -421,6 +423,7 @@ AC_CONFIG_FILES([
        contrib/nautilus/Makefile
        contrib/rg/Makefile
        contrib/search/Makefile
+       contrib/tmpl/Makefile
        contrib/xml/Makefile
 
        libide/ide-debug.h
diff --git a/contrib/Makefile.am b/contrib/Makefile.am
index d01f238..dcfefa4 100644
--- a/contrib/Makefile.am
+++ b/contrib/Makefile.am
@@ -6,6 +6,7 @@ SUBDIRS = \
        nautilus \
        rg \
        search \
+       tmpl \
        xml \
        $(NULL)
 
diff --git a/contrib/tmpl/Makefile.am b/contrib/tmpl/Makefile.am
new file mode 100644
index 0000000..78b31e5
--- /dev/null
+++ b/contrib/tmpl/Makefile.am
@@ -0,0 +1,52 @@
+pkglibdir = $(libdir)/gnome-builder
+pkglib_LTLIBRARIES = libtemplate-glib.la
+
+libtemplate_glib_la_SOURCES = \
+       tmpl-branch-node.c \
+       tmpl-branch-node.h \
+       tmpl-condition-node.c \
+       tmpl-condition-node.h \
+       tmpl-error.c \
+       tmpl-error.h \
+       tmpl-expr.c \
+       tmpl-expr.h \
+       tmpl-expr-parse.c \
+       tmpl-expr-private.h \
+       tmpl-expr-node.c \
+       tmpl-expr-node.h \
+       tmpl-iter-node.c \
+       tmpl-iter-node.h \
+       tmpl-iterator.c \
+       tmpl-iterator.h \
+       tmpl-lexer.c \
+       tmpl-lexer.h \
+       tmpl-node.c \
+       tmpl-node.h \
+       tmpl-parser.c \
+       tmpl-parser.h \
+       tmpl-scope.c \
+       tmpl-scope.h \
+       tmpl-template.c \
+       tmpl-template.h \
+       tmpl-template-locator.c \
+       tmpl-template-locator.h \
+       tmpl-text-node.c \
+       tmpl-text-node.h \
+       tmpl-token.c \
+       tmpl-token.h \
+       tmpl-token-input-stream.c \
+       tmpl-token-input-stream.h \
+       tmpl.h
+
+libtemplate_glib_la_LIBADD = $(TMPL_LIBS)
+libtemplate_glib_la_CFLAGS = $(TMPL_CFLAGS)
+libtemplate_glib_la_CPPFLAGS = -DTMPL_COMPILATION
+
+
+noinst_PROGRAMS = tmpl
+
+tmpl_SOURCES = tmpl.c
+tmpl_LDADD = libtemplate-glib.la
+tmpl_CFLAGS = $(TMPL_CFLAGS)
+
+-include $(top_srcdir)/git.mk
diff --git a/contrib/tmpl/configure.ac.tmpl b/contrib/tmpl/configure.ac.tmpl
new file mode 100644
index 0000000..40db4c9
--- /dev/null
+++ b/contrib/tmpl/configure.ac.tmpl
@@ -0,0 +1,203 @@
+AC_PREREQ([{{autoconf_required_version}}])
+
+dnl ***********************************************************************
+dnl Define Versioning Information
+dnl ***********************************************************************
+m4_define([major_version],[{{version_major}}])
+m4_define([minor_version],[{{version_minor}}])
+m4_define([micro_version],[{{version_micro}}])
+m4_define([version],[major_version.minor_version.micro_version])
+m4_define([interface_age],[0])
+m4_define([bugreport_url],[{{bugreport_url}}])
+m4_define([debug_default],[m4_if(m4_eval(minor_version % 2),[1],[yes],[minimum])])
+
+
+dnl ***********************************************************************
+dnl Initialize Autoconf
+dnl ***********************************************************************
+AC_INIT([{{name}}],[version],[bugreport_url])
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_SRCDIR([configure.ac])
+AC_CONFIG_MACRO_DIR([build-aux])
+AC_CONFIG_AUX_DIR([build-aux])
+AC_SUBST(ACLOCAL_AMFLAGS, "-I build-aux")
+AC_CANONICAL_HOST
+
+
+dnl ***********************************************************************
+dnl Export version information
+dnl ***********************************************************************
+MAJOR_VERSION=major_version
+MINOR_VERSION=minor_version
+MICRO_VERSION=micro_version
+AC_SUBST(MAJOR_VERSION)
+AC_SUBST(MINOR_VERSION)
+AC_SUBST(MICRO_VERSION)
+
+
+dnl ***********************************************************************
+dnl Initialize Automake
+dnl ***********************************************************************
+AM_SILENT_RULES([yes])
+AM_INIT_AUTOMAKE([1.11 foreign subdir-objects tar-ustar no-dist-gzip dist-xz])
+AM_MAINTAINER_MODE([enable])
+
+
+dnl ***********************************************************************
+dnl Internationalization
+dnl ***********************************************************************
+IT_PROG_INTLTOOL([0.50.1])
+GETTEXT_PACKAGE=AC_PACKAGE_TARNAME
+AC_SUBST(GETTEXT_PACKAGE)
+AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE], ["$GETTEXT_PACKAGE"], [GETTEXT package name])
+AM_GLIB_GNU_GETTEXT
+
+
+dnl ***********************************************************************
+dnl Check for Required Programs
+dnl ***********************************************************************
+AC_PROG_CC
+{{if enable_cplusplus}}
+AC_PROG_CXX
+{{endif}}
+AC_PROG_INSTALL
+AC_PATH_PROG([GLIB_GENMARSHAL], [glib-genmarshal])
+AC_PATH_PROG([GLIB_MKENUMS], [glib-mkenums])
+AC_PATH_PROG([GLIB_COMPILE_RESOURCES], [glib-compile-resources])
+GLIB_GSETTINGS
+PKG_PROG_PKG_CONFIG([0.22])
+{{if enable_gobject_introspection}}
+GOBJECT_INTROSPECTION_CHECK([1.42.0])
+{{endif}}
+{{if enable_vala}}
+AM_PROG_VALAC
+VAPIGEN_CHECK
+{{endif}}
+{{if enable_appstream}}
+APPSTREAM_XML
+{{endif}}
+
+
+{{if enable_c11}}
+dnl ***********************************************************************
+dnl Ensure C11 is Supported
+dnl ***********************************************************************
+AX_CHECK_COMPILE_FLAG([-std=gnu11],
+                      [CFLAGS="$CFLAGS -std=gnu11"],
+                      [AC_MSG_ERROR([C compiler cannot compile GNU C11 code])])
+
+
+{{endif}}
+dnl ***********************************************************************
+dnl Setup Debug and Tracing Support
+dnl ***********************************************************************
+AC_ARG_ENABLE(debug,
+              AS_HELP_STRING([--enable-debug=@<:@no/minimum/yes@:>@],
+                             [turn on debugging @<:@default=debug_default@:>@]),
+              ,
+              enable_debug=debug_default)
+AS_CASE(["$enable_debug"],
+        [yes],[
+            DEBUG_CFLAGS="$DEBUG_CFLAGS -O0"
+            DEBUG_CFLAGS="$DEBUG_CFLAGS -g"
+        ],
+        [minimum],[
+            DEBUG_CFLAGS="$DEBUG_CFLAGS -DG_DISABLE_CAST_CHECKS"
+        ],
+        [no],[
+            DEBUG_CFLAGS="$DEBUG_CFLAGS -DG_DISABLE_ASSERT"
+            DEBUG_CFLAGS="$DEBUG_CFLAGS -DG_DISABLE_CHECKS"
+            DEBUG_CFLAGS="$DEBUG_CFLAGS -DG_DISABLE_CAST_CHECKS"
+        ],
+        [])
+AC_SUBST(DEBUG_CFLAGS)
+
+
+dnl ***********************************************************************
+dnl Check for Required Packages
+dnl ***********************************************************************
+m4_define([glib_required_version],[{{glib_required_version}}])
+
+PKG_CHECK_MODULES({{name|strup|delimit("- ",'_')}}, [
+       gobject-2.0 >= glib_required_version
+       gio-2.0 >= glib_required_version
+])
+
+
+dnl ***********************************************************************
+dnl Initialize Libtool
+dnl ***********************************************************************
+LT_PREREQ([2.2])
+LT_INIT
+
+
+{{if enable_compiler_flags}}
+dnl ***********************************************************************
+dnl Additional C Compiler Flags
+dnl ***********************************************************************
+AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option], [
+       ax_compiler_flags_test="-Werror=unknown-warning-option"
+], [
+       ax_compiler_flags_test=""
+])
+AX_APPEND_COMPILE_FLAGS([ \
+       -Wall \
+       -Wcast-align \
+       -Wdeclaration-after-statement \
+       -Wempty-body \
+       -Werror=format-security \
+       -Werror=format=2 \
+       -Wextra \
+       -Wformat \
+       -Wformat-nonliteral \
+       -Wformat-security \
+       -Winit-self \
+       -Wmisleading-indentation \
+       -Wmissing-include-dirs \
+       -Wshift-negative-value \
+       -Wnested-externs \
+       -Wno-missing-field-initializers \
+       -Wno-sign-compare \
+       -Wno-strict-aliasing \
+       -Wno-uninitialized \
+       -Wno-unused-parameter \
+       -Wpointer-arith \
+       -Wredundant-decls \
+       -Wreturn-type \
+       -Wshadow \
+       -Wswitch-default \
+       -Wswitch-enum \
+       -Wundef \
+       -Wuninitialized \
+], [], [$ax_compiler_flags_test])
+AC_C_CONST
+
+
+{{endif}}
+{{if enable_gtk_doc}}
+dnl ***********************************************************************
+dnl Support for gtk-doc Documentation Engine
+dnl ***********************************************************************
+GTK_DOC_CHECK([1.11],[--flavour no-tmpl])
+AM_CONDITIONAL(ENABLE_GTK_DOC, test "x$enable_gtk_doc" = "xyes")
+AC_ARG_ENABLE(doc-cross-references,
+              AS_HELP_STRING([--disable-doc-cross-references],
+                             [cross reference symbols from other libraries @<:@default=yes@:>@]),
+              enable_doc_cross_references=$enableval,
+              enable_doc_cross_references=yes)
+AM_CONDITIONAL(ENABLE_DOC_CROSS_REFERENCES, test x$enable_doc_cross_references != xno)
+
+
+{{endif}}
+dnl ***********************************************************************
+dnl Process .in Files
+dnl ***********************************************************************
+AC_CONFIG_FILES([
+       Makefile
+])
+AC_OUTPUT
+
+
+echo ""
+echo " ${PACKAGE} - ${VERSION}"
+echo ""
diff --git a/contrib/tmpl/tmpl-branch-node.c b/contrib/tmpl/tmpl-branch-node.c
new file mode 100644
index 0000000..0592788
--- /dev/null
+++ b/contrib/tmpl/tmpl-branch-node.c
@@ -0,0 +1,221 @@
+/* tmpl-branch-node.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-branch-node.h"
+#include "tmpl-condition-node.h"
+#include "tmpl-error.h"
+
+struct _TmplBranchNode
+{
+  TmplNode   parent_instance;
+
+  TmplNode  *if_branch;
+  GPtrArray *children;
+};
+
+G_DEFINE_TYPE (TmplBranchNode, tmpl_branch_node, TMPL_TYPE_NODE)
+
+static gboolean
+tmpl_branch_node_accept (TmplNode      *node,
+                         TmplLexer     *lexer,
+                         GCancellable  *cancellable,
+                         GError       **error)
+{
+  TmplBranchNode *self = (TmplBranchNode *)node;
+
+  g_assert (TMPL_IS_BRANCH_NODE (self));
+  g_assert (self->if_branch != NULL);
+  g_assert (lexer != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!tmpl_node_accept (self->if_branch, lexer, cancellable, error))
+    return FALSE;
+
+  /*
+   * At this point, the if branch should have everything, so we are
+   * looking for ELSE_IF, ELSE, or END. Everything else is a syntax
+   * error.
+   */
+
+  while (TRUE)
+    {
+      TmplToken *token = NULL;
+
+      if (!tmpl_lexer_next (lexer, &token, cancellable, error))
+        return FALSE;
+
+      switch (tmpl_token_type (token))
+        {
+        case TMPL_TOKEN_END:
+          tmpl_token_free (token);
+          return TRUE;
+
+        case TMPL_TOKEN_ELSE:
+        case TMPL_TOKEN_ELSE_IF:
+          {
+            TmplNode *child;
+            TmplExpr *expr;
+            const gchar *exprstr;
+
+            exprstr = tmpl_token_get_text (token);
+            expr = tmpl_expr_from_string (exprstr, error);
+            tmpl_token_free (token);
+
+            if (expr == NULL)
+              return FALSE;
+
+            child = tmpl_condition_node_new (expr);
+
+            if (self->children == NULL)
+              self->children = g_ptr_array_new_with_free_func (g_object_unref);
+            g_ptr_array_add (self->children, child);
+
+            if (!tmpl_node_accept (child, lexer, cancellable, error))
+              return FALSE;
+          }
+          break;
+
+        case TMPL_TOKEN_TEXT:
+        case TMPL_TOKEN_IF:
+        case TMPL_TOKEN_EXPRESSION:
+        case TMPL_TOKEN_FOR:
+        case TMPL_TOKEN_INCLUDE:
+        default:
+          tmpl_token_free (token);
+          g_set_error (error,
+                       TMPL_ERROR,
+                       TMPL_ERROR_SYNTAX_ERROR,
+                       "Invalid token, expected else if, else, or end.");
+          return FALSE;
+        }
+    }
+}
+
+static void
+tmpl_branch_node_visit_children (TmplNode        *node,
+                                 TmplNodeVisitor  visitor,
+                                 gpointer         user_data)
+{
+  TmplBranchNode *self = (TmplBranchNode *)node;
+
+  g_assert (TMPL_IS_NODE (node));
+  g_assert (visitor != NULL);
+
+  if (self->if_branch)
+    visitor (self->if_branch, user_data);
+
+  if (self->children != NULL)
+    {
+      gint i;
+
+      for (i = 0; i < self->children->len; i++)
+        {
+          TmplNode *child = g_ptr_array_index (self->children, i);
+
+          visitor (child, user_data);
+        }
+    }
+}
+
+static void
+tmpl_branch_node_finalize (GObject *object)
+{
+  TmplBranchNode *self = (TmplBranchNode *)object;
+
+  g_clear_pointer (&self->children, g_ptr_array_unref);
+  g_clear_object (&self->if_branch);
+
+  G_OBJECT_CLASS (tmpl_branch_node_parent_class)->finalize (object);
+}
+
+static void
+tmpl_branch_node_class_init (TmplBranchNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  TmplNodeClass *node_class = TMPL_NODE_CLASS (klass);
+
+  object_class->finalize = tmpl_branch_node_finalize;
+
+  node_class->accept = tmpl_branch_node_accept;
+  node_class->visit_children = tmpl_branch_node_visit_children;
+}
+
+static void
+tmpl_branch_node_init (TmplBranchNode *self)
+{
+}
+
+/**
+ * tmpl_branch_node_new:
+ * @expr: (transfer full): A TmplExpr
+ *
+ * Creates a new branch (if, else if, else) using @expr for the (if)
+ * branch.
+ *
+ * Returns: (transfer full): A #TmplBranchNode.
+ */
+TmplNode *
+tmpl_branch_node_new (TmplExpr *condition)
+{
+  TmplBranchNode *self;
+
+  self = g_object_new (TMPL_TYPE_BRANCH_NODE, NULL);
+  self->if_branch = tmpl_condition_node_new (condition);
+
+  return TMPL_NODE (self);
+}
+
+static gboolean
+condition_matches (TmplNode  *condition,
+                   TmplScope *scope)
+{
+  TmplExpr *expr;
+
+  g_assert (TMPL_IS_CONDITION_NODE (condition));
+
+  if (!(expr = tmpl_condition_node_get_condition (TMPL_CONDITION_NODE (condition))))
+    return FALSE;
+
+  return tmpl_expr_eval_boolean (expr, scope);
+}
+
+TmplNode *
+tmpl_branch_node_branch (TmplBranchNode *self,
+                         TmplScope      *scope)
+{
+  g_return_val_if_fail (TMPL_IS_BRANCH_NODE (self), NULL);
+  g_return_val_if_fail (self->if_branch != NULL, NULL);
+
+  if (condition_matches (self->if_branch, scope))
+    return self->if_branch;
+
+  if (self->children != NULL)
+    {
+      gint i;
+
+      for (i = 0; i < self->children->len; i++)
+        {
+          TmplNode *child = g_ptr_array_index (self->children, i);
+
+          if (condition_matches (child, scope))
+            return child;
+        }
+    }
+
+  return NULL;
+}
diff --git a/contrib/tmpl/tmpl-branch-node.h b/contrib/tmpl/tmpl-branch-node.h
new file mode 100644
index 0000000..efd3cbb
--- /dev/null
+++ b/contrib/tmpl/tmpl-branch-node.h
@@ -0,0 +1,38 @@
+/* tmpl-branch-node.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_BRANCH_NODE_H
+#define TMPL_BRANCH_NODE_H
+
+#include "tmpl-expr.h"
+#include "tmpl-node.h"
+#include "tmpl-scope.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_BRANCH_NODE (tmpl_branch_node_get_type())
+
+G_DECLARE_FINAL_TYPE (TmplBranchNode, tmpl_branch_node, TMPL, BRANCH_NODE, TmplNode)
+
+TmplNode *tmpl_branch_node_new    (TmplExpr       *condition);
+TmplNode *tmpl_branch_node_branch (TmplBranchNode *self,
+                                   TmplScope      *scope);
+
+G_END_DECLS
+
+#endif /* TMPL_BRANCH_NODE_H */
diff --git a/contrib/tmpl/tmpl-condition-node.c b/contrib/tmpl/tmpl-condition-node.c
new file mode 100644
index 0000000..a83e37d
--- /dev/null
+++ b/contrib/tmpl/tmpl-condition-node.c
@@ -0,0 +1,155 @@
+/* tmpl-condition-node.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-condition-node.h"
+
+struct _TmplConditionNode
+{
+  TmplNode  parent_instance;
+
+  GPtrArray *children;
+  TmplExpr  *condition;
+};
+
+G_DEFINE_TYPE (TmplConditionNode, tmpl_condition_node, TMPL_TYPE_NODE)
+
+static gboolean
+tmpl_condition_node_accept (TmplNode      *node,
+                            TmplLexer     *lexer,
+                            GCancellable  *cancellable,
+                            GError       **error)
+{
+  TmplConditionNode *self = (TmplConditionNode *)node;
+  TmplToken *token;
+
+  g_assert (TMPL_IS_NODE (node));
+  g_assert (token != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /*
+   * We are an if/else if/else condition, if we come across an
+   * end/else if/else then we need to unget and allow the parent
+   * branch to resolve.
+   */
+
+  while (TRUE)
+    {
+      TmplNode *child;
+
+      if (!tmpl_lexer_next (lexer, &token, cancellable, error))
+        return FALSE;
+
+      switch (tmpl_token_type (token))
+        {
+        case TMPL_TOKEN_ELSE_IF:
+        case TMPL_TOKEN_ELSE:
+        case TMPL_TOKEN_END:
+          tmpl_lexer_unget (lexer, token);
+          return TRUE;
+
+        case TMPL_TOKEN_TEXT:
+        case TMPL_TOKEN_IF:
+        case TMPL_TOKEN_FOR:
+        case TMPL_TOKEN_EXPRESSION:
+          if (self->children == NULL)
+            self->children = g_ptr_array_new_with_free_func (g_object_unref);
+
+          child = tmpl_node_new_for_token (token);
+          g_ptr_array_add (self->children, child);
+
+          if (!tmpl_node_accept (child, lexer, cancellable, error))
+            return FALSE;
+
+          break;
+
+        case TMPL_TOKEN_INCLUDE:
+        default:
+          g_warning ("Invalid token type");
+          return FALSE;
+        }
+    }
+}
+
+static void
+tmpl_condition_node_visit_children (TmplNode        *node,
+                                    TmplNodeVisitor  visitor,
+                                    gpointer         user_data)
+{
+  TmplConditionNode *self = (TmplConditionNode *)node;
+
+  g_assert (TMPL_IS_CONDITION_NODE (self));
+  g_assert (visitor != NULL);
+
+  if (self->children != NULL)
+    {
+      gint i;
+
+      for (i = 0; i < self->children->len; i++)
+        {
+          TmplNode *child = g_ptr_array_index (self->children, i);
+          visitor (child, user_data);
+        }
+    }
+}
+
+static void
+tmpl_condition_node_finalize (GObject *object)
+{
+  TmplConditionNode *self = (TmplConditionNode *)object;
+
+  g_clear_pointer (&self->condition, tmpl_expr_free);
+  g_clear_pointer (&self->children, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (tmpl_condition_node_parent_class)->finalize (object);
+}
+
+static void
+tmpl_condition_node_class_init (TmplConditionNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  TmplNodeClass *node_class = TMPL_NODE_CLASS (klass);
+
+  object_class->finalize = tmpl_condition_node_finalize;
+
+  node_class->accept = tmpl_condition_node_accept;
+  node_class->visit_children = tmpl_condition_node_visit_children;
+}
+
+static void
+tmpl_condition_node_init (TmplConditionNode *self)
+{
+}
+
+TmplNode *
+tmpl_condition_node_new (TmplExpr *condition)
+{
+  TmplConditionNode *self;
+
+  self = g_object_new (TMPL_TYPE_CONDITION_NODE, NULL);
+  self->condition = condition;
+
+  return TMPL_NODE (self);
+}
+
+TmplExpr *
+tmpl_condition_node_get_condition (TmplConditionNode *self)
+{
+  g_return_val_if_fail (TMPL_IS_CONDITION_NODE (self), NULL);
+
+  return self->condition;
+}
diff --git a/contrib/tmpl/tmpl-condition-node.h b/contrib/tmpl/tmpl-condition-node.h
new file mode 100644
index 0000000..41b46b0
--- /dev/null
+++ b/contrib/tmpl/tmpl-condition-node.h
@@ -0,0 +1,36 @@
+/* tmpl-condition-node.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_CONDITION_NODE_H
+#define TMPL_CONDITION_NODE_H
+
+#include "tmpl-expr.h"
+#include "tmpl-node.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_CONDITION_NODE (tmpl_condition_node_get_type())
+
+G_DECLARE_FINAL_TYPE (TmplConditionNode, tmpl_condition_node, TMPL, CONDITION_NODE, TmplNode)
+
+TmplNode *tmpl_condition_node_new           (TmplExpr          *expr);
+TmplExpr *tmpl_condition_node_get_condition (TmplConditionNode *self);
+
+G_END_DECLS
+
+#endif /* TMPL_CONDITION_NODE_H */
diff --git a/contrib/tmpl/tmpl-error.c b/contrib/tmpl/tmpl-error.c
new file mode 100644
index 0000000..3306183
--- /dev/null
+++ b/contrib/tmpl/tmpl-error.c
@@ -0,0 +1,25 @@
+/* tmpl-error.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-error.h"
+
+GQuark
+tmpl_error_quark (void)
+{
+  return g_quark_from_static_string ("tmpl-error");
+}
diff --git a/contrib/tmpl/tmpl-error.h b/contrib/tmpl/tmpl-error.h
new file mode 100644
index 0000000..8ddfbbc
--- /dev/null
+++ b/contrib/tmpl/tmpl-error.h
@@ -0,0 +1,40 @@
+/* tmpl-error.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_ERROR_H
+#define TMPL_ERROR_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#define TMPL_ERROR (tmpl_error_quark())
+
+typedef enum
+{
+  TMPL_ERROR_INVALID_STATE = 1,
+  TMPL_ERROR_TEMPLATE_NOT_FOUND,
+  TMPL_ERROR_CIRCULAR_INCLUDE,
+  TMPL_ERROR_SYNTAX_ERROR,
+} TmplError;
+
+GQuark tmpl_error_quark (void);
+
+G_END_DECLS
+
+#endif /* TMPL_ERROR_H */
diff --git a/contrib/tmpl/tmpl-expr-node.c b/contrib/tmpl/tmpl-expr-node.c
new file mode 100644
index 0000000..f3fb91c
--- /dev/null
+++ b/contrib/tmpl/tmpl-expr-node.c
@@ -0,0 +1,97 @@
+/* tmpl-expr-node.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-expr-node.h"
+
+struct _TmplExprNode
+{
+  TmplNode  parent_instance;
+  TmplExpr *expr;
+};
+
+G_DEFINE_TYPE (TmplExprNode, tmpl_expr_node, TMPL_TYPE_NODE)
+
+static gboolean
+tmpl_expr_node_accept (TmplNode      *node,
+                       TmplLexer     *lexer,
+                       GCancellable  *cancellable,
+                       GError       **error)
+{
+  /* no children */
+  return TRUE;
+}
+
+static void
+tmpl_expr_node_finalize (GObject *object)
+{
+  TmplExprNode *self = (TmplExprNode *)object;
+
+  g_clear_pointer (&self->expr, tmpl_expr_free);
+
+  G_OBJECT_CLASS (tmpl_expr_node_parent_class)->finalize (object);
+}
+
+static void
+tmpl_expr_node_class_init (TmplExprNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  TmplNodeClass *node_class = TMPL_NODE_CLASS (klass);
+
+  object_class->finalize = tmpl_expr_node_finalize;
+
+  node_class->accept = tmpl_expr_node_accept;
+}
+
+static void
+tmpl_expr_node_init (TmplExprNode *self)
+{
+}
+
+/**
+ * tmpl_expr_node_new:
+ * @expr: (transfer full): The expression
+ *
+ * Creates a new node, stealing the reference to @expr.
+ *
+ * Returns: (transfer full): A #TmplExprNode
+ */
+TmplNode *
+tmpl_expr_node_new (TmplExpr *expr)
+{
+  TmplExprNode *self;
+
+  self = g_object_new (TMPL_TYPE_EXPR_NODE, NULL);
+  self->expr = expr;
+
+  return TMPL_NODE (self);
+}
+
+/**
+ * tmpl_expr_node_get_expr:
+ *
+ * Gets the expression.
+ *
+ * Returns: (transfer none): A #TmplExpr.
+ */
+TmplExpr *
+tmpl_expr_node_get_expr (TmplExprNode *self)
+{
+  g_return_val_if_fail (TMPL_IS_EXPR_NODE (self), NULL);
+
+  return self->expr;
+}
diff --git a/contrib/tmpl/tmpl-expr-node.h b/contrib/tmpl/tmpl-expr-node.h
new file mode 100644
index 0000000..4ba2910
--- /dev/null
+++ b/contrib/tmpl/tmpl-expr-node.h
@@ -0,0 +1,36 @@
+/* tmpl-expr-node.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_EXPR_NODE_H
+#define TMPL_EXPR_NODE_H
+
+#include "tmpl-expr.h"
+#include "tmpl-node.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_EXPR_NODE (tmpl_expr_node_get_type())
+
+G_DECLARE_FINAL_TYPE (TmplExprNode, tmpl_expr_node, TMPL, EXPR_NODE, TmplNode)
+
+TmplNode *tmpl_expr_node_new      (TmplExpr     *expr);
+TmplExpr *tmpl_expr_node_get_expr (TmplExprNode *self);
+
+G_END_DECLS
+
+#endif /* TMPL_EXPR_NODE_H */
diff --git a/contrib/tmpl/tmpl-expr-parse.c b/contrib/tmpl/tmpl-expr-parse.c
new file mode 100644
index 0000000..2174f0e
--- /dev/null
+++ b/contrib/tmpl/tmpl-expr-parse.c
@@ -0,0 +1,34 @@
+/* tmpl-expr-parse.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-expr.h"
+#include "tmpl-expr-private.h"
+
+TmplExpr *
+tmpl_expr_from_string (const gchar  *expr,
+                       GError      **error)
+{
+  TmplExpr *self;
+
+  self = g_slice_new0 (TmplExpr);
+
+  /* TODO: finish up EBNF for GJS/Python like dot expressions */
+  /* TODO: simple function calls for processing based on G-I */
+
+  return self;
+}
diff --git a/contrib/tmpl/tmpl-expr-private.h b/contrib/tmpl/tmpl-expr-private.h
new file mode 100644
index 0000000..e83e767
--- /dev/null
+++ b/contrib/tmpl/tmpl-expr-private.h
@@ -0,0 +1,33 @@
+/* tmpl-expr-private.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_EXPR_PRIVATE_H
+#define TMPL_EXPR_PRIVATE_H
+
+#include "tmpl-expr.h"
+
+G_BEGIN_DECLS
+
+struct _TmplExpr
+{
+  void *dummy;
+};
+
+G_END_DECLS
+
+#endif /* TMPL_EXPR_PRIVATE_H */
diff --git a/contrib/tmpl/tmpl-expr.c b/contrib/tmpl/tmpl-expr.c
new file mode 100644
index 0000000..3de738a
--- /dev/null
+++ b/contrib/tmpl/tmpl-expr.c
@@ -0,0 +1,123 @@
+/* tmpl-expr.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-expr.h"
+#include "tmpl-expr-private.h"
+
+void
+tmpl_expr_free (TmplExpr *self)
+{
+  if (self != NULL)
+    {
+      g_slice_free (TmplExpr, self);
+    }
+}
+
+static gboolean
+value_as_boolean (const GValue *value)
+{
+  GValue bool_value = G_VALUE_INIT;
+  gboolean ret = FALSE;
+
+  g_value_init (&bool_value, G_TYPE_BOOLEAN);
+
+  if (g_value_transform (value, &bool_value))
+    ret = g_value_get_boolean (&bool_value);
+  else if (G_VALUE_HOLDS_STRING (value))
+    {
+      const gchar *tmp = g_value_get_string (value);
+      ret = (tmp != NULL) && (*tmp != '\0');
+    }
+  else if (G_VALUE_HOLDS_INT (value))
+    ret = g_value_get_int (value) != 0;
+  else if (G_VALUE_HOLDS_UINT (value))
+    ret = g_value_get_uint (value) != 0;
+  else if (G_VALUE_HOLDS_LONG (value))
+    ret = g_value_get_long (value) != 0;
+  else if (G_VALUE_HOLDS_ULONG (value))
+    ret = g_value_get_ulong (value) != 0;
+  else if (G_VALUE_HOLDS_INT64 (value))
+    ret = g_value_get_int64 (value) != 0;
+  else if (G_VALUE_HOLDS_UINT64 (value))
+    ret = g_value_get_uint64 (value) != 0;
+  else if (G_VALUE_HOLDS_DOUBLE (value))
+    ret = g_value_get_double (value) != 0.0;
+  else if (G_VALUE_HOLDS_FLOAT (value))
+    ret = g_value_get_float (value) != 0.0;
+  else if (G_VALUE_HOLDS_BOXED (value))
+    ret = g_value_get_boxed (value) != NULL;
+  else if (G_VALUE_HOLDS_CHAR (value))
+    ret = g_value_get_schar (value) != '\0';
+  else if (G_VALUE_HOLDS_UCHAR (value))
+    ret = g_value_get_uchar (value) != '\0';
+  else if (G_VALUE_HOLDS_VARIANT (value))
+    {
+      GVariant *var = g_value_get_variant (value);
+
+      if (g_variant_is_of_type (var, G_VARIANT_TYPE_BOOLEAN))
+        ret = g_variant_get_boolean (var);
+      else if (g_variant_is_of_type (var, G_VARIANT_TYPE_STRING))
+        {
+          const gchar *tmp = g_variant_get_string (var, NULL);
+          ret = (tmp != NULL) && (*tmp != '\0');
+        }
+    }
+
+  return ret;
+}
+
+gboolean
+tmpl_expr_eval_boolean (TmplExpr  *self,
+                        TmplScope *scope)
+{
+  GValue return_value = G_VALUE_INIT;
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+
+  if (tmpl_expr_eval (self, scope, &return_value, NULL))
+    ret = value_as_boolean (&return_value);
+
+  g_value_unset (&return_value);
+
+  return ret;
+}
+
+gboolean
+tmpl_expr_eval (TmplExpr   *self,
+                TmplScope  *scope,
+                GValue     *return_value,
+                GError    **error)
+{
+  TmplScope *local = NULL;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+  g_return_val_if_fail (return_value != NULL, FALSE);
+
+  if (scope == NULL)
+    scope = local = tmpl_scope_new (NULL);
+
+  /* XXX: temp */
+  g_value_init (return_value, G_TYPE_BOOLEAN);
+  g_value_set_boolean (return_value, TRUE);
+
+  if (local != NULL)
+    tmpl_scope_free (scope);
+
+  return TRUE;
+}
diff --git a/contrib/tmpl/tmpl-expr.h b/contrib/tmpl/tmpl-expr.h
new file mode 100644
index 0000000..9503e09
--- /dev/null
+++ b/contrib/tmpl/tmpl-expr.h
@@ -0,0 +1,42 @@
+/* tmpl-expr.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_EXPR_H
+#define TMPL_EXPR_H
+
+#include <glib-object.h>
+
+#include "tmpl-scope.h"
+
+G_BEGIN_DECLS
+
+typedef struct _TmplExpr TmplExpr;
+
+TmplExpr *tmpl_expr_from_string  (const gchar  *str,
+                                  GError      **error);
+gboolean  tmpl_expr_eval_boolean (TmplExpr     *self,
+                                  TmplScope    *scope);
+gboolean  tmpl_expr_eval         (TmplExpr     *self,
+                                  TmplScope    *scope,
+                                  GValue       *return_value,
+                                  GError      **error);
+void      tmpl_expr_free         (TmplExpr     *self);
+
+G_END_DECLS
+
+#endif /* TMPL_EXPR_H */
diff --git a/contrib/tmpl/tmpl-iter-node.c b/contrib/tmpl/tmpl-iter-node.c
new file mode 100644
index 0000000..34b1b92
--- /dev/null
+++ b/contrib/tmpl/tmpl-iter-node.c
@@ -0,0 +1,158 @@
+/* tmpl-iter-node.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-iter-node.h"
+
+struct _TmplIterNode
+{
+  TmplNode   parent_instance;
+
+  gchar     *identifier;
+  TmplExpr  *expr;
+  GPtrArray *children;
+};
+
+G_DEFINE_TYPE (TmplIterNode, tmpl_iter_node, TMPL_TYPE_NODE)
+
+static gboolean
+tmpl_iter_node_accept (TmplNode      *node,
+                       TmplLexer     *lexer,
+                       GCancellable  *cancellable,
+                       GError       **error)
+{
+  TmplIterNode *self = (TmplIterNode *)node;
+
+  g_assert (TMPL_IS_ITER_NODE (self));
+  g_assert (lexer != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  while (TRUE)
+    {
+      TmplToken *token;
+      TmplNode *child;
+
+      if (!tmpl_lexer_next (lexer, &token, cancellable, error))
+        return FALSE;
+
+      if (tmpl_token_type (token) == TMPL_TOKEN_END)
+        {
+          tmpl_token_free (token);
+          return TRUE;
+        }
+
+      if (NULL != (child = tmpl_node_new_for_token (token)))
+        {
+          g_ptr_array_add (self->children, child);
+
+          if (!tmpl_node_accept (child, lexer, cancellable, error))
+            return FALSE;
+        }
+    }
+}
+
+static void
+tmpl_iter_node_visit_children (TmplNode        *node,
+                               TmplNodeVisitor  visitor,
+                               gpointer         user_data)
+{
+  TmplIterNode *self = (TmplIterNode *)node;
+  gint i;
+
+  g_assert (TMPL_IS_ITER_NODE (self));
+  g_assert (visitor != NULL);
+
+  for (i = 0; i < self->children->len; i++)
+    {
+      TmplNode *child = g_ptr_array_index (self->children, i);
+
+      visitor (child, user_data);
+    }
+}
+
+static void
+tmpl_iter_node_finalize (GObject *object)
+{
+  TmplIterNode *self = (TmplIterNode *)object;
+
+  g_clear_pointer (&self->identifier, g_free);
+  g_clear_pointer (&self->expr, tmpl_expr_free);
+  g_clear_pointer (&self->children, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (tmpl_iter_node_parent_class)->finalize (object);
+}
+
+static void
+tmpl_iter_node_class_init (TmplIterNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  TmplNodeClass *node_class = TMPL_NODE_CLASS (klass);
+
+  object_class->finalize = tmpl_iter_node_finalize;
+
+  node_class->accept = tmpl_iter_node_accept;
+  node_class->visit_children = tmpl_iter_node_visit_children;
+}
+
+static void
+tmpl_iter_node_init (TmplIterNode *self)
+{
+  self->children = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+/**
+ * tmpl_iter_node_new:
+ * @identifier: the name of the variable inside the loop.
+ * @expr: (transfer full): A #TmplExpr.
+ *
+ * Returns: (transfer full): A #TmplIterNode.
+ */
+TmplNode *
+tmpl_iter_node_new (const gchar *identifier,
+                    TmplExpr    *expr)
+{
+  TmplIterNode *self;
+
+  g_return_val_if_fail (expr != NULL, NULL);
+
+  self = g_object_new (TMPL_TYPE_ITER_NODE, NULL);
+  self->identifier = g_strdup (identifier);
+  self->expr = expr;
+
+  return TMPL_NODE (self);
+}
+
+/**
+ * tmpl_iter_node_get_expr:
+ *
+ * Returns: (transfer none): An #TmplExpr.
+ */
+TmplExpr *
+tmpl_iter_node_get_expr (TmplIterNode *self)
+{
+  g_return_val_if_fail (TMPL_IS_ITER_NODE (self), NULL);
+
+  return self->expr;
+}
+
+const gchar *
+tmpl_iter_node_get_identifier (TmplIterNode *self)
+{
+  g_return_val_if_fail (TMPL_IS_ITER_NODE (self), NULL);
+
+  return self->identifier;
+}
diff --git a/contrib/tmpl/tmpl-iter-node.h b/contrib/tmpl/tmpl-iter-node.h
new file mode 100644
index 0000000..d0df269
--- /dev/null
+++ b/contrib/tmpl/tmpl-iter-node.h
@@ -0,0 +1,38 @@
+/* tmpl-iter-node.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_ITER_NODE_H
+#define TMPL_ITER_NODE_H
+
+#include "tmpl-expr.h"
+#include "tmpl-node.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_ITER_NODE (tmpl_iter_node_get_type())
+
+G_DECLARE_FINAL_TYPE (TmplIterNode, tmpl_iter_node, TMPL, ITER_NODE, TmplNode)
+
+TmplNode    *tmpl_iter_node_new            (const gchar  *identifier,
+                                            TmplExpr     *expr);
+TmplExpr    *tmpl_iter_node_get_expr       (TmplIterNode *self);
+const gchar *tmpl_iter_node_get_identifier (TmplIterNode *self);
+
+G_END_DECLS
+
+#endif /* TMPL_ITER_NODE_H */
diff --git a/contrib/tmpl/tmpl-iterator.c b/contrib/tmpl/tmpl-iterator.c
new file mode 100644
index 0000000..1446d68
--- /dev/null
+++ b/contrib/tmpl/tmpl-iterator.c
@@ -0,0 +1,142 @@
+/* tmpl-iterator.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <gio/gio.h>
+#include <string.h>
+
+#include "tmpl-iterator.h"
+
+typedef gboolean (*GetValue) (TmplIterator *iter,
+                              GValue       *value);
+typedef gboolean (*MoveNext) (TmplIterator *iter);
+typedef void     (*Destroy)  (TmplIterator *iter);
+
+static gboolean
+string_move_next (TmplIterator *iter)
+{
+  if (iter->instance)
+    {
+      iter->instance = g_utf8_next_char ((gchar *)iter->instance);
+      return (*(gchar *)iter->instance) != 0;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+string_get_value (TmplIterator *iter,
+                  GValue       *value)
+{
+  if (iter->instance)
+    {
+      gunichar ch = g_utf8_get_char ((gchar *)iter->instance);
+      gchar str[8];
+
+      str [g_unichar_to_utf8 (ch, str)] = '\0';
+      g_value_init (value, G_TYPE_STRING);
+      g_value_set_string (value, str);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+list_model_move_next (TmplIterator *iter)
+{
+  guint index = GPOINTER_TO_INT (iter->data1);
+  guint n_items = GPOINTER_TO_INT (iter->data2);
+
+  if (++index < n_items)
+    {
+      iter->data1 = GINT_TO_POINTER (index);
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+list_model_get_value (TmplIterator *iter,
+                      GValue       *value)
+{
+  guint index = GPOINTER_TO_INT (iter->data1);
+  GObject *obj;
+
+  obj = g_list_model_get_item (iter->instance, index);
+
+  if (obj != NULL)
+    g_value_init (value, G_OBJECT_TYPE (obj));
+  else
+    g_value_init (value, G_TYPE_OBJECT);
+
+  g_value_take_object (value, obj);
+
+  return TRUE;
+}
+
+void
+tmpl_iterator_init (TmplIterator *iter,
+                    const GValue *value)
+{
+  memset (iter, 0, sizeof *iter);
+
+  if (value == NULL)
+    return;
+
+  if (G_VALUE_HOLDS_STRING (value))
+    {
+      iter->instance = (gchar *)g_value_get_string (value);
+      iter->move_next = string_move_next;
+      iter->get_value = string_get_value;
+      iter->destroy = NULL;
+    }
+  else if (G_VALUE_HOLDS (value, G_TYPE_LIST_MODEL))
+    {
+      iter->instance = g_value_get_object (value);
+      iter->move_next = list_model_move_next;
+      iter->get_value = list_model_get_value;
+      iter->destroy = NULL;
+    }
+  /* TODO: More iter types */
+}
+
+gboolean
+tmpl_iterator_next (TmplIterator *iter)
+{
+  if (iter == NULL || iter->move_next == NULL)
+    return FALSE;
+
+  return ((MoveNext)iter->move_next) (iter);
+}
+
+void
+tmpl_iterator_get_value (TmplIterator *iter,
+                         GValue       *value)
+{
+  ((GetValue)iter->get_value) (iter, value);
+}
+
+void
+tmpl_iterator_destroy (TmplIterator *iter)
+{
+  if (iter->destroy != NULL)
+    ((Destroy)(iter->destroy)) (iter);
+  memset (iter, 0, sizeof *iter);
+}
diff --git a/contrib/tmpl/tmpl-iterator.h b/contrib/tmpl/tmpl-iterator.h
new file mode 100644
index 0000000..1d83cd5
--- /dev/null
+++ b/contrib/tmpl/tmpl-iterator.h
@@ -0,0 +1,50 @@
+/* tmpl-iterator.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_ITERATOR_H
+#define TMPL_ITERATOR_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _TmplIterator TmplIterator;
+
+struct _TmplIterator
+{
+  /*< private >*/
+  gpointer instance;  /* Data */
+  gpointer move_next; /* MoveNext */
+  gpointer get_value; /* GetValue */
+  gpointer destroy;   /* Destroy */
+  gpointer data1;
+  gpointer data2;
+  gpointer data3;
+  gpointer data4;
+};
+
+void     tmpl_iterator_init      (TmplIterator *self,
+                                  const GValue *value);
+gboolean tmpl_iterator_next      (TmplIterator *self);
+void     tmpl_iterator_get_value (TmplIterator *self,
+                                  GValue       *value);
+void     tmpl_iterator_destroy   (TmplIterator *self);
+
+G_END_DECLS
+
+#endif /* TMPL_ITERATOR_H */
diff --git a/contrib/tmpl/tmpl-lexer.c b/contrib/tmpl/tmpl-lexer.c
new file mode 100644
index 0000000..cdab1ab
--- /dev/null
+++ b/contrib/tmpl/tmpl-lexer.c
@@ -0,0 +1,183 @@
+/* tmpl-lexer.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-error.h"
+#include "tmpl-lexer.h"
+#include "tmpl-template-locator.h"
+#include "tmpl-token-input-stream.h"
+
+struct _TmplLexer
+{
+  GQueue               *stream_stack;
+  TmplTemplateLocator  *locator;
+  GHashTable           *circular;
+  GSList               *unget;
+};
+
+G_DEFINE_POINTER_TYPE (TmplLexer, tmpl_lexer)
+
+TmplLexer *
+tmpl_lexer_new (GInputStream        *stream,
+                TmplTemplateLocator *locator)
+{
+  TmplLexer *self;
+
+  g_return_val_if_fail (G_IS_INPUT_STREAM (stream), NULL);
+  g_return_val_if_fail (!locator || TMPL_IS_TEMPLATE_LOCATOR (locator), NULL);
+
+  self = g_slice_new0 (TmplLexer);
+  self->stream_stack = g_queue_new ();
+  self->locator = locator ? g_object_ref (locator) : tmpl_template_locator_new ();
+  self->circular = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  g_queue_push_head (self->stream_stack, tmpl_token_input_stream_new (stream));
+
+  return self;
+}
+
+void
+tmpl_lexer_free (TmplLexer *self)
+{
+  if (self != NULL)
+    {
+      const GList *iter;
+
+      for (iter = self->stream_stack->head; iter != NULL; iter = iter->next)
+        {
+          TmplTokenInputStream *stream = iter->data;
+
+          g_object_unref (stream);
+        }
+
+      g_clear_pointer (&self->circular, g_hash_table_unref);
+      g_clear_pointer (&self->stream_stack, g_queue_free);
+      g_clear_object (&self->locator);
+      g_slice_free (TmplLexer, self);
+    }
+}
+
+/**
+ * tmpl_lexer_next:
+ * @self: A #TmplLexer.
+ * @token: (out) (transfer full): A location for a #TmplToken.
+ * @cancellable: (nullable): A #GCancellable or %NULL.
+ * @error: A location for a #GError or %NULL.
+ *
+ * Reads the next token.
+ *
+ * It is possible for %FALSE to be returned and @error left unset.
+ * This happens at the end of the stream.
+ *
+ * Returns: %TRUE if @token was set, otherwise %FALSE.
+ */
+gboolean
+tmpl_lexer_next (TmplLexer     *self,
+                 TmplToken    **token,
+                 GCancellable  *cancellable,
+                 GError       **error)
+{
+  TmplTokenInputStream *stream;
+  GError *local_error = NULL;
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+  g_return_val_if_fail (token != NULL, FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  if (self->unget != NULL)
+    {
+      *token = self->unget->data;
+      self->unget = g_slist_remove_link (self->unget, self->unget);
+      return TRUE;
+    }
+
+  while ((stream = g_queue_peek_head (self->stream_stack)))
+    {
+      if (!(*token = tmpl_token_input_stream_read_token (stream, cancellable, &local_error)))
+        {
+          /*
+           * If we finished this stream, try to move to the next one.
+           */
+          if (local_error == NULL)
+            {
+              g_queue_pop_head (self->stream_stack);
+              g_object_unref (stream);
+              continue;
+            }
+
+          goto finish;
+        }
+
+      /*
+       * If the current token is an include token, we need to resolve the
+       * include path and read tokens from it.
+       */
+      if (tmpl_token_type (*token) == TMPL_TOKEN_INCLUDE)
+        {
+          const gchar *path = tmpl_token_include_get_path (*token);
+          GInputStream *input;
+
+          if (g_hash_table_contains (self->circular, path))
+            {
+              local_error = g_error_new (TMPL_ERROR,
+                                         TMPL_ERROR_CIRCULAR_INCLUDE,
+                                         "A circular include was detected: \"%s\"",
+                                         path);
+              g_clear_pointer (token, tmpl_token_free);
+              goto finish;
+            }
+
+          if (!(input = tmpl_template_locator_locate (self->locator, path, &local_error)))
+            {
+              g_clear_pointer (token, tmpl_token_free);
+              goto finish;
+            }
+
+          g_hash_table_insert (self->circular, g_strdup (path), NULL);
+
+          stream = tmpl_token_input_stream_new (input);
+          g_queue_push_head (self->stream_stack, stream);
+
+          g_clear_pointer (token, tmpl_token_free);
+          g_object_unref (input);
+
+          continue;
+        }
+
+      ret = TRUE;
+      break;
+    }
+
+finish:
+  if ((ret == FALSE) && (local_error != NULL))
+    g_propagate_error (error, local_error);
+
+  g_assert (ret == FALSE || *token != NULL);
+
+  return ret;
+}
+
+void
+tmpl_lexer_unget (TmplLexer *self,
+                  TmplToken *token)
+{
+  g_return_if_fail (self != NULL);
+  g_return_if_fail (token != NULL);
+
+  self->unget = g_slist_prepend (self->unget, token);
+}
diff --git a/contrib/tmpl/tmpl-lexer.h b/contrib/tmpl/tmpl-lexer.h
new file mode 100644
index 0000000..8c5c9b4
--- /dev/null
+++ b/contrib/tmpl/tmpl-lexer.h
@@ -0,0 +1,49 @@
+/* tmpl-lexer.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (TMPL_INSIDE) && !defined (TMPL_COMPILATION)
+# error "Only <tmpl.h> can be included directly."
+#endif
+
+#ifndef TMPL_LEXER_H
+#define TMPL_LEXER_H
+
+#include <gio/gio.h>
+
+#include "tmpl-token.h"
+#include "tmpl-template-locator.h"
+
+G_BEGIN_DECLS
+
+typedef struct _TmplLexer TmplLexer;
+
+GType      tmpl_lexer_get_type (void);
+TmplLexer *tmpl_lexer_new      (GInputStream         *stream,
+                                TmplTemplateLocator  *locator);
+void       tmpl_lexer_free     (TmplLexer            *self);
+void       tmpl_lexer_unget    (TmplLexer            *self,
+                                TmplToken            *token);
+gboolean   tmpl_lexer_next     (TmplLexer            *self,
+                                TmplToken           **token,
+                                GCancellable         *cancellable,
+                                GError              **error);
+
+
+G_END_DECLS
+
+#endif /* TMPL_LEXER_H */
diff --git a/contrib/tmpl/tmpl-node.c b/contrib/tmpl/tmpl-node.c
new file mode 100644
index 0000000..a7fd54b
--- /dev/null
+++ b/contrib/tmpl/tmpl-node.c
@@ -0,0 +1,298 @@
+/* tmpl-node.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tmpl-branch-node.h"
+#include "tmpl-expr-node.h"
+#include "tmpl-iter-node.h"
+#include "tmpl-node.h"
+#include "tmpl-parser.h"
+#include "tmpl-text-node.h"
+
+typedef struct
+{
+  GPtrArray *children;
+} TmplNodePrivate;
+
+typedef struct
+{
+  GString *str;
+  gint     depth;
+} TmplNodePrintf;
+
+static void tmpl_node_printf_string (TmplNode *self,
+                                     GString  *str,
+                                     gint      depth);
+
+G_DEFINE_TYPE_WITH_PRIVATE (TmplNode, tmpl_node, G_TYPE_OBJECT)
+
+static gboolean
+tmpl_node_real_accept (TmplNode      *self,
+                       TmplLexer     *lexer,
+                       GCancellable  *cancellable,
+                       GError       **error)
+{
+  TmplNodePrivate *priv = tmpl_node_get_instance_private (self);
+  TmplToken *token = NULL;
+  TmplNode *child;
+
+  g_assert (TMPL_IS_NODE (self));
+  g_assert (lexer != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  while (TRUE)
+    {
+      if (!tmpl_lexer_next (lexer, &token, cancellable, error))
+        return FALSE;
+
+      if (priv->children == NULL)
+        priv->children = g_ptr_array_new_with_free_func (g_object_unref);
+
+      switch (tmpl_token_type (token))
+        {
+        case TMPL_TOKEN_TEXT:
+        case TMPL_TOKEN_EXPRESSION:
+        case TMPL_TOKEN_IF:
+        case TMPL_TOKEN_FOR:
+          if ((child = tmpl_node_new_for_token (token)))
+            {
+              g_ptr_array_add (priv->children, child);
+              if (!tmpl_node_accept (child, lexer, cancellable, error))
+                return FALSE;
+            }
+          break;
+
+        case TMPL_TOKEN_ELSE_IF:
+        case TMPL_TOKEN_ELSE:
+        case TMPL_TOKEN_END:
+        case TMPL_TOKEN_INCLUDE:
+        default:
+          /* XXX: Better location propagation */
+          g_warning ("Received invalid token \"%s\"",
+                     tmpl_token_get_text (token));
+          break;
+        }
+
+      tmpl_token_free (token);
+    }
+
+  return TRUE;
+}
+
+static void
+tmpl_node_real_visit_children (TmplNode        *self,
+                               TmplNodeVisitor  visitor,
+                               gpointer         user_data)
+{
+  TmplNodePrivate *priv = tmpl_node_get_instance_private (self);
+  gint i;
+
+  g_assert (TMPL_IS_NODE (self));
+  g_assert (visitor != NULL);
+
+  if (priv->children != NULL)
+    {
+      for (i = 0; i < priv->children->len; i++)
+        {
+          TmplNode *child = g_ptr_array_index (priv->children, i);
+
+          visitor (child, user_data);
+        }
+    }
+}
+
+static void
+tmpl_node_finalize (GObject *object)
+{
+  TmplNode *self = (TmplNode *)object;
+  TmplNodePrivate *priv = tmpl_node_get_instance_private (self);
+
+  g_clear_pointer (&priv->children, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (tmpl_node_parent_class)->finalize (object);
+}
+
+static void
+tmpl_node_class_init (TmplNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = tmpl_node_finalize;
+
+  klass->accept = tmpl_node_real_accept;
+  klass->visit_children = tmpl_node_real_visit_children;
+}
+
+static void
+tmpl_node_init (TmplNode *self)
+{
+}
+
+TmplNode *
+tmpl_node_new (void)
+{
+  return g_object_new (TMPL_TYPE_NODE, NULL);
+}
+
+gboolean
+tmpl_node_accept (TmplNode      *self,
+                  TmplLexer     *lexer,
+                  GCancellable  *cancellable,
+                  GError       **error)
+{
+  g_return_val_if_fail (TMPL_IS_NODE (self), FALSE);
+  g_return_val_if_fail (lexer != NULL, FALSE);
+
+  return TMPL_NODE_GET_CLASS (self)->accept (self, lexer, cancellable, error);
+}
+
+void
+tmpl_node_visit_children (TmplNode        *self,
+                          TmplNodeVisitor  visitor,
+                          gpointer         user_data)
+{
+  g_return_if_fail (TMPL_IS_NODE (self));
+  g_return_if_fail (visitor != NULL);
+
+  return TMPL_NODE_GET_CLASS (self)->visit_children (self, visitor, user_data);
+}
+
+TmplNode *
+tmpl_node_new_for_token (TmplToken *token)
+{
+  g_return_val_if_fail (token != NULL, NULL);
+
+  switch (tmpl_token_type (token))
+    {
+    case TMPL_TOKEN_TEXT:
+      return tmpl_text_node_new (g_strdup (tmpl_token_get_text (token)));
+
+    case TMPL_TOKEN_IF:
+      {
+        TmplExpr *expr;
+        const gchar *exprstr;
+
+        exprstr = tmpl_token_get_text (token);
+
+        if (!(expr = tmpl_expr_from_string (exprstr, NULL)))
+          return NULL;
+
+        return tmpl_branch_node_new (expr);
+      }
+
+    case TMPL_TOKEN_FOR:
+      {
+        const gchar *item_in_expr;
+        TmplExpr *expr;
+        TmplNode *node = NULL;
+        char *item = NULL;
+        char *exprstr = NULL;
+
+        if (!(item_in_expr = tmpl_token_get_text (token)))
+          return NULL;
+
+        if (2 != sscanf (item_in_expr, "%ms in %ms", &item, &exprstr))
+          goto for_cleanup;
+
+        if (!(expr = tmpl_expr_from_string (exprstr, NULL)))
+          goto for_cleanup;
+
+        node = tmpl_iter_node_new (item, expr);
+
+      for_cleanup:
+        free (item);
+        free (exprstr);
+
+        return node;
+      }
+
+    case TMPL_TOKEN_EXPRESSION:
+      {
+        TmplExpr *expr;
+        const gchar *exprstr;
+
+        exprstr = tmpl_token_get_text (token);
+
+        if (!(expr = tmpl_expr_from_string (exprstr, NULL)))
+          return NULL;
+
+        return tmpl_expr_node_new (expr);
+      }
+
+    case TMPL_TOKEN_ELSE_IF:
+    case TMPL_TOKEN_ELSE:
+    case TMPL_TOKEN_END:
+    case TMPL_TOKEN_INCLUDE:
+    default:
+      return NULL;
+    }
+}
+
+static void
+tmpl_node_printf_visitor (TmplNode *node,
+                          gpointer  user_data)
+{
+  TmplNodePrintf *state = user_data;
+
+  g_assert (TMPL_IS_NODE (node));
+  g_assert (state != NULL);
+  g_assert (state->str != NULL);
+  g_assert (state->depth > 0);
+
+  tmpl_node_printf_string (node, state->str, state->depth);
+}
+
+static void
+tmpl_node_printf_string (TmplNode *self,
+                         GString  *str,
+                         gint      depth)
+{
+  TmplNodePrintf state = { str, depth + 1 };
+  gint i;
+
+  g_assert (TMPL_IS_NODE (self));
+  g_assert (str != NULL);
+
+  for (i = 0; i < depth; i++)
+    g_string_append (str, "  ");
+  g_string_append (str, G_OBJECT_TYPE_NAME (self));
+  g_string_append_c (str, '\n');
+
+  tmpl_node_visit_children (self,
+                            tmpl_node_printf_visitor,
+                            &state);
+}
+
+gchar *
+tmpl_node_printf (TmplNode *self)
+{
+  GString *str;
+
+  g_return_val_if_fail (TMPL_IS_NODE (self), NULL);
+
+  str = g_string_new (NULL);
+  tmpl_node_printf_string (self, str, 0);
+
+  return g_string_free (str, FALSE);
+}
diff --git a/contrib/tmpl/tmpl-node.h b/contrib/tmpl/tmpl-node.h
new file mode 100644
index 0000000..b2e59c9
--- /dev/null
+++ b/contrib/tmpl/tmpl-node.h
@@ -0,0 +1,65 @@
+/* tmpl-node.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (TMPL_INSIDE) && !defined (TMPL_COMPILATION)
+# error "Only <tmpl.h> can be included directly."
+#endif
+
+#ifndef TMPL_NODE_H
+#define TMPL_NODE_H
+
+#include <gio/gio.h>
+
+#include "tmpl-lexer.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_NODE (tmpl_node_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (TmplNode, tmpl_node, TMPL, NODE, GObject)
+
+typedef void (*TmplNodeVisitor) (TmplNode *self,
+                                 gpointer  user_data);
+
+struct _TmplNodeClass
+{
+  GObjectClass parent_class;
+
+  gboolean (*accept)         (TmplNode        *self,
+                              TmplLexer       *lexer,
+                              GCancellable    *cancellable,
+                              GError         **error);
+  void     (*visit_children) (TmplNode        *self,
+                              TmplNodeVisitor  visitor,
+                              gpointer         user_data);
+};
+
+TmplNode *tmpl_node_new            (void);
+TmplNode *tmpl_node_new_for_token  (TmplToken        *token);
+gboolean  tmpl_node_accept         (TmplNode         *self,
+                                    TmplLexer        *lexer,
+                                    GCancellable     *cancellable,
+                                    GError          **error);
+gchar    *tmpl_node_printf         (TmplNode         *self);
+void      tmpl_node_visit_children (TmplNode         *self,
+                                    TmplNodeVisitor   visitor,
+                                    gpointer          user_data);
+
+G_END_DECLS
+
+#endif /* TMPL_NODE_H */
diff --git a/contrib/tmpl/tmpl-parser.c b/contrib/tmpl/tmpl-parser.c
new file mode 100644
index 0000000..4942907
--- /dev/null
+++ b/contrib/tmpl/tmpl-parser.c
@@ -0,0 +1,267 @@
+/* tmpl-parser.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "tmpl-error.h"
+#include "tmpl-lexer.h"
+#include "tmpl-node.h"
+#include "tmpl-parser.h"
+
+struct _TmplParser
+{
+  GObject               parent_instance;
+
+  TmplNode             *root;
+  GInputStream         *stream;
+  TmplTemplateLocator  *locator;
+
+  guint                 has_parsed : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_LOCATOR,
+  PROP_STREAM,
+  LAST_PROP
+};
+
+G_DEFINE_TYPE (TmplParser, tmpl_parser, G_TYPE_OBJECT)
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+tmpl_parser_set_stream (TmplParser   *self,
+                        GInputStream *stream)
+{
+  g_assert (TMPL_IS_PARSER (self));
+  g_assert (!stream || G_IS_INPUT_STREAM (stream));
+
+  if (stream == NULL)
+    {
+      g_warning ("TmplParser created without a stream!");
+      return;
+    }
+
+  g_set_object (&self->stream, stream);
+}
+
+static void
+tmpl_parser_finalize (GObject *object)
+{
+  TmplParser *self = (TmplParser *)object;
+
+  g_clear_object (&self->locator);
+  g_clear_object (&self->stream);
+  g_clear_object (&self->root);
+
+  G_OBJECT_CLASS (tmpl_parser_parent_class)->finalize (object);
+}
+
+static void
+tmpl_parser_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  TmplParser *self = TMPL_PARSER(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      g_value_set_object (value, tmpl_parser_get_locator (self));
+      break;
+
+    case PROP_STREAM:
+      g_value_set_object (value, self->stream);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+tmpl_parser_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  TmplParser *self = TMPL_PARSER(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      tmpl_parser_set_locator (self, g_value_get_object (value));
+      break;
+
+    case PROP_STREAM:
+      tmpl_parser_set_stream (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+tmpl_parser_class_init (TmplParserClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = tmpl_parser_finalize;
+  object_class->get_property = tmpl_parser_get_property;
+  object_class->set_property = tmpl_parser_set_property;
+
+  properties [PROP_LOCATOR] =
+    g_param_spec_object ("locator",
+                         "Locator",
+                         "The template locator for resolving includes",
+                         TMPL_TYPE_TEMPLATE_LOCATOR,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_STREAM] =
+    g_param_spec_object ("stream",
+                         "Stream",
+                         "The stream to parse",
+                         G_TYPE_INPUT_STREAM,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+tmpl_parser_init (TmplParser *self)
+{
+  self->root = tmpl_node_new ();
+}
+
+TmplParser *
+tmpl_parser_new (GInputStream *stream)
+{
+  g_return_val_if_fail (G_IS_INPUT_STREAM (stream), NULL);
+
+  return g_object_new (TMPL_TYPE_PARSER,
+                       "stream", stream,
+                       NULL);
+}
+
+/**
+ * tmpl_parser_get_root:
+ * @self: A #TmplNode.
+ *
+ * Gets the root node for the parser.
+ *
+ * See tmpl_parser_visit_children() to apply a visitor to all nodes created
+ * by the parser.
+ *
+ * Returns: (transfer none): An #TmplNode.
+ */
+TmplNode *
+tmpl_parser_get_root (TmplParser *self)
+{
+  g_return_val_if_fail (TMPL_IS_PARSER (self), NULL);
+
+  return self->root;
+}
+
+gboolean
+tmpl_parser_parse (TmplParser    *self,
+                   GCancellable  *cancellable,
+                   GError       **error)
+{
+  TmplLexer *lexer;
+  GError *local_error = NULL;
+
+  g_return_val_if_fail (TMPL_IS_PARSER (self), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  if (self->has_parsed)
+    {
+      g_set_error (error,
+                   TMPL_ERROR,
+                   TMPL_ERROR_INVALID_STATE,
+                   _("%s() may only be called once"),
+                   G_STRFUNC);
+      return FALSE;
+    }
+
+  self->has_parsed = TRUE;
+
+  if (self->stream == NULL)
+    {
+      g_set_error (error,
+                   TMPL_ERROR,
+                   TMPL_ERROR_INVALID_STATE,
+                   _("Parser does not contain an input stream"));
+      return FALSE;
+    }
+
+  lexer = tmpl_lexer_new (self->stream, self->locator);
+  tmpl_node_accept (self->root, lexer, cancellable, &local_error);
+  tmpl_lexer_free (lexer);
+
+  if (local_error != NULL)
+    {
+      g_propagate_error (error, local_error);
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+/**
+ * tmpl_parser_get_locator:
+ * @self: an #TmplParser
+ *
+ * Gets the template loader used for resolving includes when parsing template
+ * files.
+ *
+ * Includes are performed using the {{include "path"}} token.  The locator can
+ * be used to restrict where the templates are located from. By default, the
+ * search path is empty, and includes cannot be performed.
+ *
+ * Returns: (transfer none): A #TmplTemplateLocator.
+ */
+TmplTemplateLocator *
+tmpl_parser_get_locator (TmplParser *self)
+{
+  g_return_val_if_fail (TMPL_IS_PARSER (self), NULL);
+
+  return self->locator;
+}
+
+/**
+ * tmpl_parser_set_locator:
+ * @self: A #TmplParser
+ * @locator: A #TmplTemplateLocator
+ *
+ * Sets the template locator used to resolve {{include "path"}} directives.
+ *
+ * See tmpl_parser_get_locator() for more information.
+ */
+void
+tmpl_parser_set_locator (TmplParser          *self,
+                         TmplTemplateLocator *locator)
+{
+  g_return_if_fail (TMPL_IS_PARSER (self));
+  g_return_if_fail (TMPL_IS_TEMPLATE_LOCATOR (locator));
+
+  if (g_set_object (&self->locator, locator))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOCATOR]);
+}
diff --git a/contrib/tmpl/tmpl-parser.h b/contrib/tmpl/tmpl-parser.h
new file mode 100644
index 0000000..da0c1dc
--- /dev/null
+++ b/contrib/tmpl/tmpl-parser.h
@@ -0,0 +1,48 @@
+/* tmpl-parser.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (TMPL_INSIDE) && !defined (TMPL_COMPILATION)
+# error "Only <tmpl.h> can be included directly."
+#endif
+
+#ifndef TMPL_PARSER_H
+#define TMPL_PARSER_H
+
+#include <gio/gio.h>
+
+#include "tmpl-node.h"
+#include "tmpl-template-locator.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_PARSER (tmpl_parser_get_type())
+
+G_DECLARE_FINAL_TYPE (TmplParser, tmpl_parser, TMPL, PARSER, GObject)
+
+TmplNode            *tmpl_parser_get_root    (TmplParser           *self);
+TmplParser          *tmpl_parser_new         (GInputStream         *stream);
+TmplTemplateLocator *tmpl_parser_get_locator (TmplParser           *self);
+void                 tmpl_parser_set_locator (TmplParser           *self,
+                                              TmplTemplateLocator  *locator);
+gboolean             tmpl_parser_parse       (TmplParser           *self,
+                                              GCancellable         *cancellable,
+                                              GError              **error);
+
+G_END_DECLS
+
+#endif /* TMPL_PARSER_H */
diff --git a/contrib/tmpl/tmpl-scope.c b/contrib/tmpl/tmpl-scope.c
new file mode 100644
index 0000000..343bf37
--- /dev/null
+++ b/contrib/tmpl/tmpl-scope.c
@@ -0,0 +1,102 @@
+/* tmpl-scope.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-scope.h"
+
+struct _TmplScope
+{
+  TmplScope  *parent;
+  GHashTable *ident_to_value;
+};
+
+TmplScope *
+tmpl_scope_new (TmplScope *parent)
+{
+  TmplScope *self;
+
+  self = g_slice_new0 (TmplScope);
+  self->parent = parent;
+
+  return self;
+}
+
+void
+tmpl_scope_free (TmplScope *self)
+{
+  if (self != NULL)
+    {
+      g_clear_pointer (&self->ident_to_value, g_hash_table_unref);
+      g_slice_free (TmplScope, self);
+    }
+}
+
+const GValue *
+tmpl_scope_lookup (TmplScope   *self,
+                   const gchar *identifier)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (identifier != NULL, NULL);
+
+  for (; self != NULL; self = self->parent)
+    {
+      if (self->ident_to_value != NULL)
+        return g_hash_table_lookup (self->ident_to_value, identifier);
+    }
+
+  return NULL;
+}
+
+static void
+value_free (gpointer data)
+{
+  GValue *value = data;
+
+  if (value != NULL)
+    {
+      g_value_unset (value);
+      g_slice_free (GValue, value);
+    }
+}
+
+void
+tmpl_scope_set (TmplScope    *self,
+                const gchar  *identifier,
+                const GValue *value)
+{
+  GValue *copy;
+
+  g_return_if_fail (self != NULL);
+
+  if (value == NULL && self->ident_to_value == NULL)
+    return;
+
+  if (self->ident_to_value == NULL)
+    self->ident_to_value = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, value_free);
+
+  if (value == NULL)
+    {
+      g_hash_table_remove (self->ident_to_value, identifier);
+      return;
+    }
+
+  copy = g_slice_new (GValue);
+  g_value_init (copy, G_VALUE_TYPE (value));
+  g_value_copy (value, copy);
+
+  g_hash_table_insert (self->ident_to_value, g_strdup (identifier), copy);
+}
diff --git a/contrib/tmpl/tmpl-scope.h b/contrib/tmpl/tmpl-scope.h
new file mode 100644
index 0000000..a8a0105
--- /dev/null
+++ b/contrib/tmpl/tmpl-scope.h
@@ -0,0 +1,38 @@
+/* tmpl-scope.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_SCOPE_H
+#define TMPL_SCOPE_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _TmplScope TmplScope;
+
+TmplScope    *tmpl_scope_new    (TmplScope    *parent);
+void          tmpl_scope_free   (TmplScope    *self);
+const GValue *tmpl_scope_lookup (TmplScope    *self,
+                                 const gchar  *identifier);
+void          tmpl_scope_set    (TmplScope    *self,
+                                 const gchar  *identifier,
+                                 const GValue *value);
+
+G_END_DECLS
+
+#endif /* TMPL_SCOPE_H */
diff --git a/contrib/tmpl/tmpl-template-locator.c b/contrib/tmpl/tmpl-template-locator.c
new file mode 100644
index 0000000..f31b8f5
--- /dev/null
+++ b/contrib/tmpl/tmpl-template-locator.c
@@ -0,0 +1,197 @@
+/* tmpl-template-locator.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "tmpl-error.h"
+#include "tmpl-template-locator.h"
+
+typedef struct
+{
+  GQueue  *search_path;
+} TmplTemplateLocatorPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (TmplTemplateLocator, tmpl_template_locator, G_TYPE_OBJECT)
+
+static GInputStream *
+tmpl_template_locator_locate_in_path (TmplTemplateLocator *self,
+                                      const gchar         *path_base,
+                                      const gchar         *path)
+{
+  GInputStream *ret = NULL;
+  gchar *full_path;
+
+  g_assert (TMPL_IS_TEMPLATE_LOCATOR (self));
+  g_assert (path_base != NULL);
+  g_assert (path != NULL);
+
+  full_path = g_build_path (path_base, path, NULL);
+
+  if (g_str_has_prefix (full_path, "resource://"))
+    {
+      /*
+       * A mediocre attempt to prevent escapes using ../
+       */
+      if (strstr (full_path, "..") == NULL)
+        ret = g_resources_open_stream (full_path + strlen ("resource://"), 0, NULL);
+    }
+  else
+    {
+      GFile *parent = g_file_new_for_path (path_base);
+      GFile *file = g_file_new_for_path (full_path);
+      gchar *relative;
+
+      /*
+       * If the path tries to escape the search path, using ../../ or
+       * something clever, we will get an invalid path here.
+       */
+      if ((relative = g_file_get_relative_path (parent, file)))
+        {
+          g_free (relative);
+          ret = (GInputStream *)g_file_read (file, NULL, NULL);
+        }
+
+      g_object_unref (parent);
+      g_object_unref (file);
+    }
+
+  g_free (full_path);
+
+  return ret;
+}
+
+static GInputStream *
+tmpl_template_locator_real_locate (TmplTemplateLocator  *self,
+                                   const gchar          *path,
+                                   GError              **error)
+{
+  TmplTemplateLocatorPrivate *priv = tmpl_template_locator_get_instance_private (self);
+  GInputStream *ret = NULL;
+  const GList *iter;
+  const GList *search_path;
+
+  g_assert (TMPL_IS_TEMPLATE_LOCATOR (self));
+  g_assert (path != NULL);
+
+  search_path = priv->search_path->head;
+
+  for (iter = search_path; ret == NULL && iter != NULL; iter = iter->next)
+    {
+      const gchar *path_base = iter->data;
+
+      ret = tmpl_template_locator_locate_in_path (self, path_base, path);
+    }
+
+  if (ret == NULL)
+    {
+      g_set_error (error,
+                   TMPL_ERROR,
+                   TMPL_ERROR_TEMPLATE_NOT_FOUND,
+                   _("Failed to locate template \"%s\""),
+                   path);
+      return NULL;
+    }
+
+  return ret;
+}
+
+static void
+tmpl_template_locator_class_init (TmplTemplateLocatorClass *klass)
+{
+  klass->locate = tmpl_template_locator_real_locate;
+}
+
+static void
+tmpl_template_locator_init (TmplTemplateLocator *self)
+{
+  TmplTemplateLocatorPrivate *priv = tmpl_template_locator_get_instance_private (self);
+
+  priv->search_path = g_queue_new ();
+}
+
+void
+tmpl_template_locator_append_search_path (TmplTemplateLocator *self,
+                                          const gchar         *path)
+{
+  TmplTemplateLocatorPrivate *priv = tmpl_template_locator_get_instance_private (self);
+
+  g_return_if_fail (TMPL_IS_TEMPLATE_LOCATOR (self));
+  g_return_if_fail (path != NULL);
+
+  g_queue_push_tail (priv->search_path, g_strdup (path));
+}
+
+void
+tmpl_template_locator_prepend_search_path (TmplTemplateLocator *self,
+                                           const gchar         *path)
+{
+  TmplTemplateLocatorPrivate *priv = tmpl_template_locator_get_instance_private (self);
+
+  g_return_if_fail (TMPL_IS_TEMPLATE_LOCATOR (self));
+  g_return_if_fail (path != NULL);
+
+  g_queue_push_head (priv->search_path, g_strdup (path));
+}
+
+/**
+ * tmpl_template_locator_get_search_path:
+ * @self: A #TmplTemplateLocator
+ *
+ * Gets the current search path used by the template locator.
+ *
+ * Returns: (transfer full): A %NULL-terminated array of strings.
+ */
+gchar **
+tmpl_template_locator_get_search_path (TmplTemplateLocator *self)
+{
+  TmplTemplateLocatorPrivate *priv = tmpl_template_locator_get_instance_private (self);
+  GPtrArray *ar;
+  const GList *iter;
+
+  g_return_val_if_fail (TMPL_IS_TEMPLATE_LOCATOR (self), NULL);
+
+  ar = g_ptr_array_new ();
+
+  for (iter = priv->search_path->head; iter != NULL; iter = iter->next)
+    {
+      const gchar *path = iter->data;
+
+      g_ptr_array_add (ar, g_strdup (path));
+    }
+
+  g_ptr_array_add (ar, NULL);
+
+  return (gchar **)g_ptr_array_free (ar, FALSE);
+}
+
+TmplTemplateLocator *
+tmpl_template_locator_new (void)
+{
+  return g_object_new (TMPL_TYPE_TEMPLATE_LOCATOR, NULL);
+}
+
+GInputStream *
+tmpl_template_locator_locate (TmplTemplateLocator  *self,
+                              const gchar          *path,
+                              GError              **error)
+{
+  g_return_val_if_fail (TMPL_IS_TEMPLATE_LOCATOR (self), NULL);
+  g_return_val_if_fail (path != NULL, NULL);
+
+  return TMPL_TEMPLATE_LOCATOR_GET_CLASS (self)->locate (self, path, error);
+}
diff --git a/contrib/tmpl/tmpl-template-locator.h b/contrib/tmpl/tmpl-template-locator.h
new file mode 100644
index 0000000..3db60f0
--- /dev/null
+++ b/contrib/tmpl/tmpl-template-locator.h
@@ -0,0 +1,57 @@
+/* tmpl-template-locator.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (TMPL_INSIDE) && !defined (TMPL_COMPILATION)
+# error "Only <tmpl.h> can be included directly."
+#endif
+
+#ifndef TMPL_TEMPLATE_LOCATOR_H
+#define TMPL_TEMPLATE_LOCATOR_H
+
+#include <gio/gio.h>
+
+#include "tmpl-template-locator.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_TEMPLATE_LOCATOR (tmpl_template_locator_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (TmplTemplateLocator, tmpl_template_locator, TMPL, TEMPLATE_LOCATOR, GObject)
+
+struct _TmplTemplateLocatorClass
+{
+  GObjectClass parent_instance;
+
+  GInputStream *(*locate) (TmplTemplateLocator  *self,
+                           const gchar          *path,
+                           GError              **error);
+};
+
+TmplTemplateLocator  *tmpl_template_locator_new                 (void);
+void                  tmpl_template_locator_append_search_path  (TmplTemplateLocator  *self,
+                                                                 const gchar          *path);
+void                  tmpl_template_locator_prepend_search_path (TmplTemplateLocator  *self,
+                                                                 const gchar          *path);
+GInputStream         *tmpl_template_locator_locate              (TmplTemplateLocator  *self,
+                                                                 const gchar          *path,
+                                                                 GError              **error);
+gchar               **tmpl_template_locator_get_search_path     (TmplTemplateLocator  *self);
+
+G_END_DECLS
+
+#endif /* TMPL_TEMPLATE_LOCATOR_H */
diff --git a/contrib/tmpl/tmpl-template.c b/contrib/tmpl/tmpl-template.c
new file mode 100644
index 0000000..a773e96
--- /dev/null
+++ b/contrib/tmpl/tmpl-template.c
@@ -0,0 +1,427 @@
+/* tmpl-template.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "tmpl-branch-node.h"
+#include "tmpl-condition-node.h"
+#include "tmpl-error.h"
+#include "tmpl-expr-node.h"
+#include "tmpl-iter-node.h"
+#include "tmpl-iterator.h"
+#include "tmpl-parser.h"
+#include "tmpl-scope.h"
+#include "tmpl-template.h"
+#include "tmpl-text-node.h"
+
+typedef struct
+{
+  TmplParser          *parser;
+  TmplTemplateLocator *locator;
+} TmplTemplatePrivate;
+
+typedef struct
+{
+  TmplTemplate   *self;
+  TmplNode       *root;
+  GString        *output;
+  TmplScope      *scope;
+  GError        **error;
+  gboolean        result;
+} TmplTemplateExpandState;
+
+G_DEFINE_TYPE_WITH_PRIVATE (TmplTemplate, tmpl_template, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_LOCATOR,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+tmpl_template_finalize (GObject *object)
+{
+  TmplTemplate *self = (TmplTemplate *)object;
+  TmplTemplatePrivate *priv = tmpl_template_get_instance_private (self);
+
+  g_clear_object (&priv->parser);
+
+  G_OBJECT_CLASS (tmpl_template_parent_class)->finalize (object);
+}
+
+static void
+tmpl_template_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  TmplTemplate *self = TMPL_TEMPLATE(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      g_value_set_object (value, tmpl_template_get_locator (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+tmpl_template_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  TmplTemplate *self = TMPL_TEMPLATE(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      tmpl_template_set_locator (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+tmpl_template_class_init (TmplTemplateClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = tmpl_template_finalize;
+  object_class->get_property = tmpl_template_get_property;
+  object_class->set_property = tmpl_template_set_property;
+
+  properties [PROP_LOCATOR] =
+    g_param_spec_object ("locator",
+                         "Locator",
+                         "The locator used for resolving includes",
+                         TMPL_TYPE_TEMPLATE_LOCATOR,
+                         (G_PARAM_READWRITE |
+                          G_PARAM_EXPLICIT_NOTIFY |
+                          G_PARAM_CONSTRUCT |
+                          G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+tmpl_template_init (TmplTemplate *self)
+{
+}
+
+TmplTemplate *
+tmpl_template_new (TmplTemplateLocator *locator)
+{
+  return g_object_new (TMPL_TYPE_TEMPLATE,
+                       "locator", locator,
+                       NULL);
+}
+
+gboolean
+tmpl_template_parse_file (TmplTemplate  *self,
+                          GFile         *file,
+                          GCancellable  *cancellable,
+                          GError       **error)
+{
+  GInputStream *stream;
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (TMPL_IS_TEMPLATE (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  stream = (GInputStream *)g_file_read (file, cancellable, error);
+
+  if (stream != NULL)
+    {
+      ret = tmpl_template_parse (self, stream, cancellable, error);
+      g_object_unref (stream);
+    }
+
+  return ret;
+}
+
+gboolean
+tmpl_template_parse_path (TmplTemplate  *self,
+                          const gchar   *path,
+                          GCancellable  *cancellable,
+                          GError       **error)
+{
+  gboolean ret;
+  GFile *file;
+
+  g_return_val_if_fail (TMPL_IS_TEMPLATE (self), FALSE);
+  g_return_val_if_fail (path != NULL, FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  file = g_file_new_for_path (path);
+  ret = tmpl_template_parse_file (self, file, cancellable, error);
+  g_object_unref (file);
+
+  return ret;
+}
+
+gboolean
+tmpl_template_parse_resource (TmplTemplate  *self,
+                              const gchar   *resource_path,
+                              GCancellable  *cancellable,
+                              GError       **error)
+{
+  gchar *copied = NULL;
+  gboolean ret;
+  GFile *file;
+
+  g_return_val_if_fail (TMPL_IS_TEMPLATE (self), FALSE);
+  g_return_val_if_fail (resource_path != NULL, FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  if (!g_str_has_prefix (resource_path, "resource://"))
+    resource_path = copied = g_strdup_printf ("resource://%s", resource_path);
+
+  file = g_file_new_for_uri (resource_path);
+  ret = tmpl_template_parse_file (self, file, cancellable, error);
+
+  g_object_unref (file);
+  g_free (copied);
+
+  return ret;
+}
+
+gboolean
+tmpl_template_parse (TmplTemplate  *self,
+                     GInputStream  *stream,
+                     GCancellable  *cancellable,
+                     GError       **error)
+{
+  TmplTemplatePrivate *priv = tmpl_template_get_instance_private (self);
+  TmplParser *parser;
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (TMPL_IS_TEMPLATE (self), FALSE);
+  g_return_val_if_fail (G_IS_INPUT_STREAM (stream), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  parser = tmpl_parser_new (stream);
+
+  tmpl_parser_set_locator (parser, priv->locator);
+
+  if (tmpl_parser_parse (parser, cancellable, error))
+    {
+      g_set_object (&priv->parser, parser);
+      ret = TRUE;
+#if 0
+      {
+        gchar *str;
+
+        str = tmpl_node_printf (tmpl_parser_get_root (parser));
+        g_print ("%s", str);
+        g_free (str);
+      }
+#endif
+    }
+
+  g_object_unref (parser);
+
+  return ret;
+}
+
+static void
+value_into_string (const GValue *value,
+                   GString      *str)
+{
+  GValue transform = G_VALUE_INIT;
+
+  g_value_init (&transform, G_TYPE_STRING);
+
+  if (g_value_transform (value, &transform))
+    {
+      const gchar *tmp;
+
+      if (NULL != (tmp = g_value_get_string (&transform)))
+        g_string_append (str, tmp);
+    }
+
+  g_value_unset (&transform);
+}
+
+static void
+tmpl_template_expand_visitor (TmplNode *node,
+                              gpointer  user_data)
+{
+  TmplTemplateExpandState *state = user_data;
+
+  g_assert (TMPL_IS_NODE (node));
+  g_assert (state != NULL);
+
+  if (TMPL_IS_TEXT_NODE (node))
+    {
+      g_string_append (state->output, tmpl_text_node_get_text (TMPL_TEXT_NODE (node)));
+    }
+  else if (TMPL_IS_EXPR_NODE (node))
+    {
+      GValue return_value = { 0 };
+      TmplExpr *expr;
+
+      expr = tmpl_expr_node_get_expr (TMPL_EXPR_NODE (node));
+
+      /* propagate error? */
+      if (!tmpl_expr_eval (expr, state->scope, &return_value, NULL))
+        return;
+
+      value_into_string (&return_value, state->output);
+      g_value_unset (&return_value);
+    }
+  else if (TMPL_IS_BRANCH_NODE (node))
+    {
+      TmplNode *child;
+
+      child = tmpl_branch_node_branch (TMPL_BRANCH_NODE (node), state->scope);
+      if (child != NULL)
+        tmpl_node_visit_children (child, tmpl_template_expand_visitor, state);
+    }
+  else if (TMPL_IS_CONDITION_NODE (node))
+    {
+      TmplExpr *expr;
+
+      expr = tmpl_condition_node_get_condition (TMPL_CONDITION_NODE (node));
+      if (tmpl_expr_eval_boolean (expr, state->scope))
+        tmpl_node_visit_children (node, tmpl_template_expand_visitor, state);
+    }
+  else if (TMPL_IS_ITER_NODE (node))
+    {
+      const gchar *identifier;
+      TmplExpr *expr;
+      GValue return_value = G_VALUE_INIT;
+
+      identifier = tmpl_iter_node_get_identifier (TMPL_ITER_NODE (node));
+      expr = tmpl_iter_node_get_expr (TMPL_ITER_NODE (node));
+
+      if (tmpl_expr_eval (expr, state->scope, &return_value, NULL))
+        {
+          TmplIterator iter;
+          TmplScope *old_scope = state->scope;
+          TmplScope *new_scope = tmpl_scope_new (old_scope);
+
+          state->scope = new_scope;
+
+          tmpl_iterator_init (&iter, &return_value);
+
+          while (tmpl_iterator_next (&iter))
+            {
+              GValue value;
+
+              tmpl_iterator_get_value (&iter, &value);
+              tmpl_scope_set (new_scope, identifier, &value);
+              tmpl_node_visit_children (node, tmpl_template_expand_visitor, state);
+              g_value_unset (&value);
+            }
+
+          state->scope = old_scope;
+          tmpl_scope_free (new_scope);
+        }
+
+      g_value_unset (&return_value);
+    }
+  else
+    {
+      g_warning ("Teach me how to expand %s", G_OBJECT_TYPE_NAME (node));
+    }
+}
+
+gboolean
+tmpl_template_expand (TmplTemplate  *self,
+                      GOutputStream *stream,
+                      TmplScope     *scope,
+                      GCancellable  *cancellable,
+                      GError       **error)
+{
+  TmplTemplatePrivate *priv = tmpl_template_get_instance_private (self);
+  TmplTemplateExpandState state = { 0 };
+
+  g_return_val_if_fail (TMPL_IS_TEMPLATE (self), FALSE);
+  g_return_val_if_fail (G_IS_OUTPUT_STREAM (stream), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  if (priv->parser == NULL)
+    {
+      g_set_error (error,
+                   TMPL_ERROR,
+                   TMPL_ERROR_INVALID_STATE,
+                   _("Must parse template before expanding"));
+      return FALSE;
+    }
+
+  state.root = tmpl_parser_get_root (priv->parser);
+  state.self = self;
+  state.output = g_string_new (NULL);
+  state.result = TRUE;
+  state.error = error;
+  state.scope = scope;
+
+  tmpl_node_visit_children (state.root, tmpl_template_expand_visitor, &state);
+
+  if (state.result != FALSE)
+    state.result = g_output_stream_write_all (stream,
+                                              state.output->str,
+                                              state.output->len,
+                                              NULL,
+                                              cancellable,
+                                              error);
+
+  g_string_free (state.output, TRUE);
+
+  return state.result;
+}
+
+/**
+ * tmpl_template_get_locator:
+ * @template: A #TmplTemplate
+ *
+ * Gets the template locator used when resolving template includes.
+ *
+ * Returns: (transfer none): a #TmplTemplateLocator or %NULL.
+ */
+TmplTemplateLocator *
+tmpl_template_get_locator (TmplTemplate *self)
+{
+  TmplTemplatePrivate *priv = tmpl_template_get_instance_private (self);
+
+  g_return_val_if_fail (TMPL_IS_TEMPLATE (self), NULL);
+
+  return priv->locator;
+}
+
+void
+tmpl_template_set_locator (TmplTemplate        *self,
+                           TmplTemplateLocator *locator)
+{
+  TmplTemplatePrivate *priv = tmpl_template_get_instance_private (self);
+
+  g_return_if_fail (TMPL_IS_TEMPLATE (self));
+  g_return_if_fail (!locator || TMPL_IS_TEMPLATE_LOCATOR (locator));
+
+  if (g_set_object (&priv->locator, locator))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOCATOR]);
+}
diff --git a/contrib/tmpl/tmpl-template.h b/contrib/tmpl/tmpl-template.h
new file mode 100644
index 0000000..8942f4d
--- /dev/null
+++ b/contrib/tmpl/tmpl-template.h
@@ -0,0 +1,66 @@
+/* tmpl-template.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_TEMPLATE_H
+#define TMPL_TEMPLATE_H
+
+#include <gio/gio.h>
+
+#include "tmpl-scope.h"
+#include "tmpl-template-locator.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_TEMPLATE (tmpl_template_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (TmplTemplate, tmpl_template, TMPL, TEMPLATE, GObject)
+
+struct _TmplTemplateClass
+{
+  GObjectClass parent_class;
+};
+
+TmplTemplate        *tmpl_template_new            (TmplTemplateLocator  *locator);
+TmplTemplateLocator *tmpl_template_get_locator    (TmplTemplate         *self);
+void                 tmpl_template_set_locator    (TmplTemplate         *self,
+                                                   TmplTemplateLocator  *locator);
+gboolean             tmpl_template_parse_file     (TmplTemplate         *self,
+                                                   GFile                *file,
+                                                   GCancellable         *cancellable,
+                                                   GError              **error);
+gboolean             tmpl_template_parse_resource (TmplTemplate         *self,
+                                                   const gchar          *path,
+                                                   GCancellable         *cancellable,
+                                                   GError              **error);
+gboolean             tmpl_template_parse_path     (TmplTemplate         *self,
+                                                   const gchar          *path,
+                                                   GCancellable         *cancellable,
+                                                   GError              **error);
+gboolean             tmpl_template_parse          (TmplTemplate         *self,
+                                                   GInputStream         *stream,
+                                                   GCancellable         *cancellable,
+                                                   GError              **error);
+gboolean             tmpl_template_expand         (TmplTemplate         *self,
+                                                   GOutputStream        *stream,
+                                                   TmplScope            *scope,
+                                                   GCancellable         *cancellable,
+                                                   GError              **error);
+
+G_END_DECLS
+
+#endif /* TMPL_TEMPLATE_H */
diff --git a/contrib/tmpl/tmpl-text-node.c b/contrib/tmpl/tmpl-text-node.c
new file mode 100644
index 0000000..8ed1972
--- /dev/null
+++ b/contrib/tmpl/tmpl-text-node.c
@@ -0,0 +1,90 @@
+/* tmpl-text-node.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-text-node.h"
+
+struct _TmplTextNode
+{
+  TmplNode  parent_instance;
+  gchar    *text;
+};
+
+G_DEFINE_TYPE (TmplTextNode, tmpl_text_node, TMPL_TYPE_NODE)
+
+static gboolean
+tmpl_text_node_accept (TmplNode      *node,
+                       TmplLexer     *lexer,
+                       GCancellable  *cancellable,
+                       GError       **error)
+{
+  /* no children */
+  return TRUE;
+}
+
+static void
+tmpl_text_node_finalize (GObject *object)
+{
+  TmplTextNode *self = (TmplTextNode *)object;
+
+  g_clear_pointer (&self->text, g_free);
+
+  G_OBJECT_CLASS (tmpl_text_node_parent_class)->finalize (object);
+}
+
+static void
+tmpl_text_node_class_init (TmplTextNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  TmplNodeClass *node_class = TMPL_NODE_CLASS (klass);
+
+  object_class->finalize = tmpl_text_node_finalize;
+
+  node_class->accept = tmpl_text_node_accept;
+}
+
+static void
+tmpl_text_node_init (TmplTextNode *node)
+{
+}
+
+/**
+ * tmpl_text_node_new:
+ * @text: (transfer full): the text for the node
+ *
+ * Creates a new text node.
+ *
+ * Returns: (transfer full): the new node.
+ */
+TmplNode *
+tmpl_text_node_new (gchar *text)
+{
+  TmplTextNode *self;
+
+  self = g_object_new (TMPL_TYPE_TEXT_NODE, NULL);
+  self->text = text;
+
+  return TMPL_NODE (self);
+}
+
+const gchar *
+tmpl_text_node_get_text (TmplTextNode *self)
+{
+  g_return_val_if_fail (TMPL_IS_TEXT_NODE (self), NULL);
+
+  return self->text;
+}
diff --git a/contrib/tmpl/tmpl-text-node.h b/contrib/tmpl/tmpl-text-node.h
new file mode 100644
index 0000000..a481c9e
--- /dev/null
+++ b/contrib/tmpl/tmpl-text-node.h
@@ -0,0 +1,37 @@
+/* tmpl-text-node.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_TEXT_NODE_H
+#define TMPL_TEXT_NODE_H
+
+#include "tmpl-node.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_TEXT_NODE (tmpl_text_node_get_type())
+
+G_DECLARE_FINAL_TYPE (TmplTextNode, tmpl_text_node, TMPL, TEXT_NODE, TmplNode)
+
+TmplNode     *tmpl_text_node_new      (gchar        *text);
+const gchar  *tmpl_text_node_get_text (TmplTextNode *self);
+void          tmpl_text_node_set_text (TmplTextNode *self,
+                                       const gchar  *text);
+
+G_END_DECLS
+
+#endif /* TMPL_TEXT_NODE_H */
diff --git a/contrib/tmpl/tmpl-token-input-stream.c b/contrib/tmpl/tmpl-token-input-stream.c
new file mode 100644
index 0000000..94d50bc
--- /dev/null
+++ b/contrib/tmpl/tmpl-token-input-stream.c
@@ -0,0 +1,307 @@
+/* tmpl-token-input-stream.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "tmpl-token-input-stream.h"
+
+struct _TmplTokenInputStream
+{
+  GDataInputStream parent_instance;
+};
+
+G_DEFINE_TYPE (TmplTokenInputStream, tmpl_token_input_stream, G_TYPE_DATA_INPUT_STREAM)
+
+static void
+tmpl_token_input_stream_class_init (TmplTokenInputStreamClass *klass)
+{
+}
+
+static void
+tmpl_token_input_stream_init (TmplTokenInputStream *self)
+{
+}
+
+static gboolean
+tmpl_token_input_stream_read_unichar (TmplTokenInputStream  *self,
+                                      gunichar              *unichar,
+                                      GCancellable          *cancellable,
+                                      GError               **error)
+{
+  GBufferedInputStream *stream = (GBufferedInputStream *)self;
+  gchar str[8] = { 0 };
+  gint c;
+  gint n;
+  gint i;
+
+  g_assert (TMPL_IS_TOKEN_INPUT_STREAM (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (-1 == (c = g_buffered_input_stream_read_byte (stream, cancellable, error)))
+    return FALSE;
+
+  if ((c & 0x80) == 0)
+    n = 1;
+  else if ((c & 0xE0) == 0xC0)
+    n = 2;
+  else if ((c & 0xF0) == 0xE0)
+    n = 3;
+  else if ((c & 0xF8) == 0xF0)
+    n = 4;
+  else if ((c & 0xFC) == 0xF8)
+    n = 5;
+  else if ((c & 0xFE) == 0xFC)
+    n = 6;
+  else
+    n = 0;
+
+  str [0] = c;
+
+  for (i = 1; i < n; i++)
+    {
+      if (-1 == (c = g_buffered_input_stream_read_byte (stream, cancellable, error)))
+        return FALSE;
+
+      str [i] = (gchar)c;
+    }
+
+  *unichar = g_utf8_get_char (str);
+
+  return TRUE;
+}
+
+static gchar *
+tmpl_token_input_stream_read_tag (TmplTokenInputStream  *self,
+                                  gsize                 *length,
+                                  GCancellable          *cancellable,
+                                  GError               **error)
+{
+  GBufferedInputStream *stream = (GBufferedInputStream *)self;
+  GByteArray *ar;
+  GError *local_error = NULL;
+  gboolean in_string = FALSE;
+  guchar byte;
+  gint c;
+
+  g_assert (TMPL_IS_TOKEN_INPUT_STREAM (self));
+  g_assert (length != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ar = g_byte_array_new ();
+
+  while (TRUE)
+    {
+      if (-1 == (c = g_buffered_input_stream_read_byte (stream, cancellable, &local_error)))
+        goto failure;
+
+      switch (c)
+        {
+        case '\\':
+          if (in_string)
+            {
+              g_byte_array_append (ar, (const guchar *)"\\", 1);
+
+              if (-1 == (c = g_buffered_input_stream_read_byte (stream, cancellable, &local_error)))
+                goto failure;
+            }
+
+          break;
+
+        case '"':
+          in_string = !in_string;
+          break;
+
+        case '}':
+          if (!in_string)
+            {
+              if (-1 == (c = g_buffered_input_stream_read_byte (stream, cancellable, &local_error)))
+                goto failure;
+
+              /* Check if we got }} */
+              if (c == '}')
+                goto finish;
+
+              g_byte_array_append (ar, (const guchar *)"}", 1);
+            }
+
+          break;
+
+        default:
+          break;
+        }
+
+      byte = (guchar)c;
+      g_byte_array_append (ar, (const guchar *)&byte, 1);
+    }
+
+finish:
+  *length = ar->len;
+
+  byte = 0;
+  g_byte_array_append (ar, (const guchar *)&byte, 1);
+
+  return (gchar *)g_byte_array_free (ar, FALSE);
+
+failure:
+  *length = 0;
+
+  g_byte_array_free (ar, TRUE);
+
+  if (local_error)
+    g_propagate_error (error, local_error);
+
+  return FALSE;
+}
+
+/**
+ * tmpl_token_input_stream_read_token:
+ * @self: An #TmplTokenInputStream
+ * @error: (nullable): An optional location for a #GError
+ *
+ * Reads the next token from the underlying stream.
+ *
+ * If there was an error, %NULL is returned and @error is set.
+ *
+ * Returns: (transfer full): A #TmplToken or %NULL.
+ */
+TmplToken *
+tmpl_token_input_stream_read_token (TmplTokenInputStream  *self,
+                                    GCancellable          *cancellable,
+                                    GError               **error)
+{
+  GDataInputStream *stream = (GDataInputStream *)self;
+  GError *local_error = NULL;
+  gunichar ch;
+  gchar *text;
+  gsize len;
+
+  g_return_val_if_fail (TMPL_IS_TOKEN_INPUT_STREAM (self), NULL);
+
+  /*
+   * The syntax of the template language is very simple. All of our symbols
+   * start with {{ and end with }}. We use \ to escape, and you only ever
+   * need to escape the opening of {{ like \{{.
+   *
+   * We scan ahead until a { or \ and take appropriate action based upon
+   * peeking at the next char.
+   *
+   * Once we resolve that, we walk forward past the expression until }}.
+   * To walk past the expression, we need to know when we are in a
+   * string, since }} could theoretically be in there too.
+   */
+
+  text = g_data_input_stream_read_upto (stream, "\\{", -1, &len, cancellable, error);
+
+  /*
+   * Handle end of stream.
+   */
+  if (text == NULL)
+    return NULL;
+
+  /*
+   * Handle successful read up to \ or {.
+   */
+  if (*text != '\0')
+    return tmpl_token_new_text (text);
+
+  g_free (text);
+
+  /*
+   * Peek what type of delimiter we hit.
+   */
+  ch = g_data_input_stream_read_byte (stream, cancellable, &local_error);
+
+  if ((ch == 0) && (local_error != NULL))
+    {
+      g_propagate_error (error, local_error);
+      return NULL;
+    }
+
+  /*
+   * Handle possible escaped \{.
+   */
+  if (ch == '\\')
+    {
+      gchar str[8] = { 0 };
+
+      /*
+       * Get the next char after \.
+       */
+      if (!tmpl_token_input_stream_read_unichar (self, &ch, cancellable, error))
+        return tmpl_token_new_unichar ('\\');
+
+      /*
+       * Handle escaping {.
+       */
+      if (ch == '{')
+        return tmpl_token_new_unichar ('{');
+
+      /*
+       * Nothing escaped, return string as it was read.
+       */
+      g_unichar_to_utf8 (ch, str);
+
+      return tmpl_token_new_text (g_strdup_printf ("\\%s", str));
+    }
+
+  g_assert (ch == '{');
+
+  /*
+   * Look for { following {. If we reached the end of the stream, just
+   * return a token for the final {.
+   */
+  if (!tmpl_token_input_stream_read_unichar (self, &ch, cancellable, error))
+    return tmpl_token_new_unichar ('{');
+
+  /*
+   * If this is not a {{, then just return a string for the pair.
+   */
+  if (ch != '{')
+    {
+      gchar str[8] = { 0 };
+
+      g_unichar_to_utf8 (ch, str);
+
+      return tmpl_token_new_text (g_strdup_printf ("{%s", str));
+    }
+
+  /*
+   * Scan ahead until we find }}.
+   */
+  if (!(text = tmpl_token_input_stream_read_tag (self, &len, cancellable, error)))
+    return NULL;
+
+  return tmpl_token_new_generic (text);
+}
+
+/**
+ * tmpl_token_input_stream_new:
+ * @base_stream: the stream to read from
+ *
+ * Creates a #TmplTokenInputStream using @base_stream for the raw
+ * text stream.
+ *
+ * Returns: (transfer full): An #TmplTokenInputStream.
+ */
+TmplTokenInputStream *
+tmpl_token_input_stream_new (GInputStream *base_stream)
+{
+  g_return_val_if_fail (G_IS_INPUT_STREAM (base_stream), NULL);
+
+  return g_object_new (TMPL_TYPE_TOKEN_INPUT_STREAM,
+                       "base-stream", base_stream,
+                       NULL);
+}
diff --git a/contrib/tmpl/tmpl-token-input-stream.h b/contrib/tmpl/tmpl-token-input-stream.h
new file mode 100644
index 0000000..e951ba6
--- /dev/null
+++ b/contrib/tmpl/tmpl-token-input-stream.h
@@ -0,0 +1,43 @@
+/* tmpl-token-input-stream.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (TMPL_INSIDE) && !defined (TMPL_COMPILATION)
+# error "Only <tmpl.h> can be included directly."
+#endif
+
+#ifndef TMPL_TOKEN_INPUT_STREAM_H
+#define TMPL_TOKEN_INPUT_STREAM_H
+
+#include <gio/gio.h>
+
+#include "tmpl-token.h"
+
+G_BEGIN_DECLS
+
+#define TMPL_TYPE_TOKEN_INPUT_STREAM (tmpl_token_input_stream_get_type())
+
+G_DECLARE_FINAL_TYPE (TmplTokenInputStream, tmpl_token_input_stream, TMPL, TOKEN_INPUT_STREAM, 
GDataInputStream)
+
+TmplTokenInputStream *tmpl_token_input_stream_new        (GInputStream          *base_stream);
+TmplToken            *tmpl_token_input_stream_read_token (TmplTokenInputStream  *self,
+                                                          GCancellable          *cancellable,
+                                                          GError               **error);
+
+G_END_DECLS
+
+#endif /* TMPL_TOKEN_INPUT_STREAM_H */
diff --git a/contrib/tmpl/tmpl-token.c b/contrib/tmpl/tmpl-token.c
new file mode 100644
index 0000000..6c2d4ff
--- /dev/null
+++ b/contrib/tmpl/tmpl-token.c
@@ -0,0 +1,170 @@
+/* tmpl-token.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "tmpl-token.h"
+
+struct _TmplToken
+{
+  TmplTokenType type;
+  gchar *text;
+};
+
+static TmplToken *
+tmpl_token_new (void)
+{
+  TmplToken *self;
+
+  self = g_slice_new0 (TmplToken);
+
+  return self;
+}
+
+void
+tmpl_token_free (TmplToken *self)
+{
+  if (self != NULL)
+    {
+      g_free (self->text);
+      g_slice_free (TmplToken, self);
+    }
+}
+
+/**
+ * tmpl_token_new_text:
+ * @text: (transfer full): The text for the token.
+ *
+ * Creates a new #TmplToken containing @text.
+ * This is a text literal type.
+ *
+ * Returns: (transfer full): A newly allocated #TmplToken.
+ */
+TmplToken *
+tmpl_token_new_text (gchar *text)
+{
+  TmplToken *self;
+
+  self = tmpl_token_new ();
+  self->type = TMPL_TOKEN_TEXT;
+  self->text = text;
+
+  return self;
+}
+
+TmplToken *
+tmpl_token_new_unichar (gunichar ch)
+{
+  gchar utf8[8];
+  gint len;
+
+  len = g_unichar_to_utf8 (ch, utf8);
+  utf8 [len] = '\0';
+
+  return tmpl_token_new_text (g_strdup (utf8));
+}
+
+TmplTokenType
+tmpl_token_type (TmplToken *self)
+{
+  g_return_val_if_fail (self != NULL, 0);
+
+  return self->type;
+}
+
+gchar *
+tmpl_token_include_get_path (TmplToken *self)
+{
+  char *path = NULL;
+
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->type == TMPL_TOKEN_INCLUDE, NULL);
+
+  if (1 == sscanf (self->text, "include \"%m[^\"]", &path))
+    {
+      gchar *tmp = g_strdup (path);
+      free (path);
+      return tmp;
+    }
+
+  return NULL;
+}
+
+TmplToken *
+tmpl_token_new_generic (gchar *text)
+{
+  TmplToken *self;
+
+  g_return_val_if_fail (text != NULL, NULL);
+
+  self = g_slice_new0 (TmplToken);
+
+  if (g_str_has_prefix (text, "if "))
+    {
+      self->type = TMPL_TOKEN_IF;
+      self->text = g_strstrip (g_strdup (text + 3));
+    }
+  else if (g_str_has_prefix (text, "else if "))
+    {
+      self->type = TMPL_TOKEN_ELSE_IF;
+      self->text = g_strstrip (g_strdup (text + 8));
+    }
+  else if (g_str_has_prefix (text, "else"))
+    {
+      self->type = TMPL_TOKEN_ELSE;
+      self->text = NULL;
+    }
+  else if (g_str_has_prefix (text, "end"))
+    {
+      self->type = TMPL_TOKEN_END;
+      self->text = NULL;
+    }
+  else if (g_str_has_prefix (text, "for "))
+    {
+      self->type = TMPL_TOKEN_FOR;
+      self->text = g_strstrip (g_strdup (text + 4));
+    }
+  else if (g_str_has_prefix (text, "include "))
+    {
+      self->type = TMPL_TOKEN_INCLUDE;
+      self->text = g_strstrip (g_strdup (text + 8));
+    }
+  else
+    {
+      self->type = TMPL_TOKEN_EXPRESSION;
+      self->text = g_strstrip (text);
+      text = NULL;
+    }
+
+  g_free (text);
+
+  return self;
+}
+
+const gchar *
+tmpl_token_get_text (TmplToken *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+
+  return self->text;
+}
diff --git a/contrib/tmpl/tmpl-token.h b/contrib/tmpl/tmpl-token.h
new file mode 100644
index 0000000..6bdbfd1
--- /dev/null
+++ b/contrib/tmpl/tmpl-token.h
@@ -0,0 +1,54 @@
+/* tmpl-token.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#if !defined (TMPL_INSIDE) && !defined (TMPL_COMPILATION)
+# error "Only <tmpl.h> can be included directly."
+#endif
+
+#ifndef TMPL_TOKEN_H
+#define TMPL_TOKEN_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _TmplToken TmplToken;
+
+typedef enum
+{
+  TMPL_TOKEN_TEXT,
+  TMPL_TOKEN_IF,
+  TMPL_TOKEN_ELSE_IF,
+  TMPL_TOKEN_ELSE,
+  TMPL_TOKEN_END,
+  TMPL_TOKEN_FOR,
+  TMPL_TOKEN_EXPRESSION,
+  TMPL_TOKEN_INCLUDE,
+} TmplTokenType;
+
+TmplToken     *tmpl_token_new_generic      (gchar     *str);
+TmplToken     *tmpl_token_new_unichar      (gunichar   ch);
+TmplToken     *tmpl_token_new_text         (gchar     *text);
+const gchar   *tmpl_token_get_text         (TmplToken *self);
+TmplTokenType  tmpl_token_type             (TmplToken *self);
+void           tmpl_token_free             (TmplToken *self);
+gchar         *tmpl_token_include_get_path (TmplToken *self);
+
+G_END_DECLS
+
+#endif /* TMPL_TOKEN_H */
diff --git a/contrib/tmpl/tmpl.c b/contrib/tmpl/tmpl.c
new file mode 100644
index 0000000..74a0ad7
--- /dev/null
+++ b/contrib/tmpl/tmpl.c
@@ -0,0 +1,74 @@
+/* tmpl.c
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <tmpl.h>
+
+#include <gio/gunixoutputstream.h>
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  TmplTemplateLocator *locator = NULL;
+  GOutputStream *stream = NULL;
+  TmplTemplate *tmpl = NULL;
+  TmplScope *scope = NULL;
+  GFile *file = NULL;
+  GError *error = NULL;
+  gint ret = EXIT_FAILURE;
+
+  if (argc != 2)
+    {
+      g_printerr ("usage: %s TEMPLATE\n", argv [0]);
+      return EXIT_FAILURE;
+    }
+
+  locator = tmpl_template_locator_new ();
+  tmpl_template_locator_prepend_search_path (locator, ".");
+  tmpl = tmpl_template_new (locator);
+  file = g_file_new_for_commandline_arg (argv [1]);
+  scope = tmpl_scope_new (NULL);
+
+  if (!tmpl_template_parse_file (tmpl, file, NULL, &error))
+    {
+      g_printerr ("%s\n", error->message);
+      g_clear_error (&error);
+      goto cleanup;
+    }
+
+  stream = g_unix_output_stream_new (STDOUT_FILENO, FALSE);
+
+  if (!tmpl_template_expand (tmpl, stream, scope, NULL, &error))
+    {
+      g_printerr ("%s\n", error->message);
+      g_clear_error (&error);
+      goto cleanup;
+    }
+
+  ret = EXIT_SUCCESS;
+
+cleanup:
+  g_clear_object (&stream);
+  g_clear_object (&locator);
+  g_clear_object (&tmpl);
+  g_clear_object (&file);
+
+  return ret;
+}
diff --git a/contrib/tmpl/tmpl.h b/contrib/tmpl/tmpl.h
new file mode 100644
index 0000000..411d7de
--- /dev/null
+++ b/contrib/tmpl/tmpl.h
@@ -0,0 +1,35 @@
+/* tmpl.h
+ *
+ * Copyright (C) 2016 Christian Hergert <chergert redhat com>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef TMPL_H
+#define TMPL_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#define TMPL_INSIDE
+# include "tmpl-error.h"
+# include "tmpl-scope.h"
+# include "tmpl-template.h"
+# include "tmpl-template-locator.h"
+#undef TMPL_INSIDE
+
+G_END_DECLS
+
+#endif /* TMPL_H */


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]