Repository: martinohmann/hcl-rs
Pull request #60 (2022-09-06 19:31:08)

feat: implement template sub-language
BREAKING CHANGE: Heredocs and quoted strings containing template interpolations and/or template directives are not parsed as `Expression::String` anymore, but end up as `Expression::TemplateExpr` (which can be further parsed into the template elements via `Template::from_expr`) instead. Expressions of kind `Expression::String` are guaranteed to not include any templating anymore.

This implements support for the template sub-language as defined in the HCL syntax spec: https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#templates

- [x] PR description
- [x] Tests
- [x] Module docs
- [x] All public API documented


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d15fd30..e2de911 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,27 @@
 # Changelog
 
+## [0.7.0](https://github.com/martinohmann/hcl-rs/compare/v0.6.5...v0.7.0) (2022-09-01)
+
+
+### ⚠ BREAKING CHANGES
+
+* this breaks some public APIs.
+
+### Bug Fixes
+
+* derive `Eq` where suggested by clippy ([307cc2c](https://github.com/martinohmann/hcl-rs/commit/307cc2cc1883a163c9074e25f7ddd3f14bad73af))
+
+
+### refactor
+
+* move formatter into `format` module ([#55](https://github.com/martinohmann/hcl-rs/issues/55)) ([ec98979](https://github.com/martinohmann/hcl-rs/commit/ec989794876632fe466928c3759586da2d78ff13))
+
+
+### Miscellaneous
+
+* **deps:** bump actions/cache from 3.0.5 to 3.0.7 ([#53](https://github.com/martinohmann/hcl-rs/issues/53)) ([77e2c70](https://github.com/martinohmann/hcl-rs/commit/77e2c70143ec991164ea8e10628f0ed09f8bf062))
+* **deps:** bump actions/cache from 3.0.7 to 3.0.8 ([#59](https://github.com/martinohmann/hcl-rs/issues/59)) ([f78e16e](https://github.com/martinohmann/hcl-rs/commit/f78e16e874f1c4352deac88d28df280f63ef022f))
+
 ## [0.6.5](https://github.com/martinohmann/hcl-rs/compare/v0.6.4...v0.6.5) (2022-08-19)
 
 
diff --git a/Cargo.toml b/Cargo.toml
index eb23938..c781373 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "hcl-rs"
-version = "0.6.5"
+version = "0.7.0"
 authors = ["Martin Ohmann <martinohmann@gmail.com>"]
 license = "MIT"
 description = "An HCL deserializer"
diff --git a/src/format/impls.rs b/src/format/impls.rs
index 8f094a6..695b74e 100644
--- a/src/format/impls.rs
+++ b/src/format/impls.rs
@@ -1,8 +1,8 @@
 use super::{private, Format, Formatter};
 use crate::{
     structure::{
-        Attribute, Block, BlockLabel, Body, Expression, Identifier, ObjectKey, RawExpression,
-        Structure,
+        Attribute, Block, BlockLabel, Body, Expression, Heredoc, HeredocStripMode, Identifier,
+        ObjectKey, RawExpression, Structure, TemplateExpr,
     },
     Map, Number, Result, Value,
 };
@@ -104,6 +104,7 @@ impl Format for Expression {
             Expression::Array(array) => format_array(fmt, array),
             Expression::Object(object) => format_object(fmt, object),
             Expression::Raw(raw) => raw.format(fmt),
+            Expression::TemplateExpr(expr) => expr.format(fmt),
         }
     }
 }
@@ -175,6 +176,51 @@ impl Format for RawExpression {
     }
 }
 
+impl private::Sealed for TemplateExpr {}
+
+impl Format for TemplateExpr {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        match self {
+            TemplateExpr::QuotedString(string) => string.format(fmt),
+            TemplateExpr::Heredoc(heredoc) => heredoc.format(fmt),
+        }
+    }
+}
+
+impl private::Sealed for Heredoc {}
+
+impl Format for Heredoc {
+    fn format<W>(&self, fmt: &mut Formatter<W>) -> Result<()>
+    where
+        W: io::Write,
+    {
+        let delimiter = self.delimiter.as_str();
+
+        fmt.write_string_fragment(self.strip.as_str())?;
+        fmt.write_string_fragment(delimiter)?;
+        fmt.write_all(b"\n")?;
+        fmt.write_string_fragment(&self.template)?;
+
+        if !self.template.ends_with('\n') {
+            fmt.write_all(b"\n")?;
+        }
+
+        match self.strip {
+            HeredocStripMode::None => {
+                fmt.write_string_fragment(delimiter)?;
+            }
+            HeredocStripMode::Indent => {
+                fmt.write_indented(fmt.current_indent, delimiter)?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
 impl private::Sealed for Identifier {}
 
 impl Format for Identifier {
diff --git a/src/format/mod.rs b/src/format/mod.rs
index 4e890fa..68ef46c 100644
--- a/src/format/mod.rs
+++ b/src/format/mod.rs
@@ -338,7 +338,7 @@ where
             self.write_all(b",\n")?;
         }
 
-        self.write_indent(self.current_indent, self.config.indent)
+        self.write_indent(self.current_indent)
     }
 
     /// Signals the end of an array value to the formatter.
@@ -353,7 +353,7 @@ where
 
         if self.has_value {
             self.write_all(b"\n")?;
-            self.write_indent(self.current_indent, self.config.indent)?;
+            self.write_indent(self.current_indent)?;
         }
 
         self.write_all(b"]")
@@ -369,7 +369,7 @@ where
     /// Signals the start of an object key to the formatter.
     fn begin_object_key(&mut self) -> io::Result<()> {
         self.write_all(b"\n")?;
-        self.write_indent(self.current_indent, self.config.indent)
+        self.write_indent(self.current_indent)
     }
 
     /// Signals the start of an object value to the formatter.
@@ -388,7 +388,7 @@ where
 
         if self.has_value {
             self.write_all(b"\n")?;
-            self.write_indent(self.current_indent, self.config.indent)?;
+            self.write_indent(self.current_indent)?;
         }
 
         self.write_all(b"}")
@@ -397,7 +397,7 @@ where
     /// Signals the start of an attribute to the formatter.
     fn begin_attribute(&mut self) -> io::Result<()> {
         self.maybe_write_newline(FormatState::AttributeStart)?;
-        self.write_indent(self.current_indent, self.config.indent)
+        self.write_indent(self.current_indent)
     }
 
     /// Signals the start of an attribute value to the formatter.
@@ -414,7 +414,7 @@ where
     /// Signals the start of a block to the formatter.
     fn begin_block(&mut self) -> io::Result<()> {
         self.maybe_write_newline(FormatState::BlockStart)?;
-        self.write_indent(self.current_indent, self.config.indent)
+        self.write_indent(self.current_indent)
     }
 
     /// Signals the start of a block body to the formatter.
@@ -428,7 +428,7 @@ where
     fn end_block(&mut self) -> io::Result<()> {
         self.state = FormatState::BlockEnd;
         self.current_indent -= 1;
-        self.write_indent(self.current_indent, self.config.indent)?;
+        self.write_indent(self.current_indent)?;
         self.write_all(b"}\n")
     }
 
@@ -456,9 +456,26 @@ where
         Ok(())
     }
 
-    fn write_indent(&mut self, n: usize, s: &[u8]) -> io::Result<()> {
+    fn write_indent(&mut self, n: usize) -> io::Result<()> {
         for _ in 0..n {
-            self.write_all(s)?;
+            self.write_all(self.config.indent)?;
+        }
+
+        Ok(())
+    }
+
+    fn write_indented(&mut self, n: usize, s: &str) -> io::Result<()> {
+        for (i, line) in s.lines().enumerate() {
+            if i > 0 {
+                self.write_all(b"\n")?;
+            }
+
+            self.write_indent(n)?;
+            self.write_string_fragment(line)?;
+        }
+
+        if s.ends_with('\n') {
+            self.write_all(b"\n")?;
         }
 
         Ok(())
diff --git a/src/lib.rs b/src/lib.rs
index 597427d..448b032 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -11,8 +11,10 @@ mod number;
 mod parser;
 pub mod ser;
 pub mod structure;
+pub mod template;
 #[cfg(test)]
 mod tests;
+mod util;
 pub mod value;
 
 #[doc(inline)]
@@ -26,7 +28,8 @@ pub use ser::{to_string, to_vec, to_writer};
 #[doc(inline)]
 pub use structure::{
     ser::to_expression, Attribute, Block, BlockBuilder, BlockLabel, Body, BodyBuilder, Expression,
-    Object, ObjectKey, RawExpression, Structure,
+    Heredoc, HeredocStripMode, Identifier, Object, ObjectKey, RawExpression, Structure,
+    TemplateExpr,
 };
 #[doc(inline)]
 pub use value::{Map, Value};
diff --git a/src/parser/grammar/hcl.pest b/src/parser/grammar/hcl.pest
index 24ea177..3f8ff43 100644
--- a/src/parser/grammar/hcl.pest
+++ b/src/parser/grammar/hcl.pest
@@ -12,10 +12,10 @@ BlockBody = { "{" ~ Body ~ "}" }
 // Expressions
 Expression    = _{ Conditional | Operation | ExprTerm }
 ExprTerm      =  { ExprTermInner ~ (Splat | GetAttr | Index)* }
-ExprTermInner = _{ Value | FunctionCall | VariableExpr | ForExpr | ("(" ~ Expression ~ ")") }
+ExprTermInner = _{ Value | TemplateExpr | FunctionCall | VariableExpr | ForExpr | ("(" ~ Expression ~ ")") }
 
 // Values
-Value = _{ LiteralValue | CollectionValue | Heredoc }
+Value = _{ LiteralValue | CollectionValue }
 
 // Literal values
 LiteralValue = _{ StringLit | NumericLit | BooleanLit | NullLit }
@@ -49,48 +49,38 @@ ObjectItem      = _{ ObjectItemIdent | ObjectItemExpr }
 ObjectItemIdent = _{ Identifier ~ ("=" | ":") ~ Expression }
 ObjectItemExpr  = _{ Expression ~ ("=" | ":") ~ Expression }
 
-// Heredoc
-Heredoc = ${
+// Template expressions
+TemplateExpr = { QuotedStringTemplate | HeredocTemplate }
+
+// Heredoc templates
+HeredocTemplate = ${
     HeredocIntro ~ PUSH(Identifier) ~ NEWLINE ~
     HeredocContent ~
     SpaceOrTab* ~ POP
 }
-HeredocIntro       = _{ (HeredocIntroIndent | HeredocIntroNormal) }
-HeredocIntroIndent =  { "<<-" }
-HeredocIntroNormal =  { "<<" }
-HeredocContent     = @{ Template ~ NEWLINE }
-
-// Templates
-//
-// @TODO(mohmann): treat everything as template literals for now as it's really hard to get
-// interpolations and directives right recursively. We are not going to consume the tokens anyways
-// right now.
-//
-// Template           =  { TemplateInterpolation | TemplateDirective | TemplateLiteral }
-Template              =  { TemplateLiteral }
-TemplateLiteral       = _{ (!(NEWLINE ~ SpaceOrTab* ~ PEEK) ~ ANY)* }
-TemplateDirective     = _{ TemplateIf | TemplateFor }
-TemplateInterpolation = !{ ("${" | "${~") ~ ("\"" | Expression) ~ ("}" | "~}") }
-
-TemplateIf = {
-    ("%{" | "%{~") ~ "if" ~ Expression ~ ("}" | "~}") ~
-    Template ~
-    (
-        ("%{" | "%{~") ~ "else" ~ ("}" | "~}") ~
-        Template
-    )? ~
-    ("%{" | "%{~") ~ "endif" ~ ("}" | "~}")
-}
-
-TemplateFor = {
-    ("%{" | "%{~") ~ "for" ~ Identifier ~ ("," ~ Identifier)? ~ "in" ~ Expression ~ ("}" | "~}") ~
-    Template ~
-    ("%{" | "%{~") ~ "endfor" ~ ("}" | "~}")
+HeredocIntro        = _{ HeredocIntroIndent | HeredocIntroNormal }
+HeredocIntroIndent  =  { "<<-" }
+HeredocIntroNormal  =  { "<<" }
+HeredocContent      = @{ HeredocContentInner ~ NEWLINE }
+HeredocContentInner = { (!(NEWLINE ~ SpaceOrTab* ~ PEEK) ~ ANY)* }
+
+// Quoted string templates
+QuotedStringTemplate      = ${ "\"" ~ QuotedStringTemplateInner ~ "\"" }
+QuotedStringTemplateInner =  {
+    (QuotedStringTemplateLiteral | TemplateInterpolation | TemplateDirective)*
 }
+QuotedStringTemplateLiteral = @{ StringPart+ }
 
 // String literals
-StringLit = ${ "\"" ~ String ~ "\"" }
-String    =  { (TemplateInterpolation | ("\\" ~ ("\"" | "\\")) | (!"\"" ~ ANY))* }
+StringLit  = ${ "\"" ~ String ~ "\"" }
+String     = @{ StringPart* }
+StringPart = {
+    "$${"
+    | "%%{"
+    | !("\"" | "\\" | "${" | "%{") ~ ANY
+    | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t")
+    | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4})
+}
 
 // Functions and function calls
 FunctionCall = @{
@@ -149,3 +139,37 @@ EoInlineComment = _{ NEWLINE | EOI }
 // Whitespace
 WHITESPACE = _{ SpaceOrTab | NEWLINE }
 SpaceOrTab = _{ " " | "\t" }
+
+// Top-level rule for the template sub-language
+HclTemplate = _{ SOI ~ Template ~ EOI }
+
+Template              = ${ (TemplateLiteral | TemplateInterpolation | TemplateDirective)* }
+TemplateLiteral       = @{ ("$${" | "%%{" | (!("${" | "%{") ~ ANY))+ }
+TemplateInterpolation = !{ TemplateIExprStart ~ Expression ~ TemplateExprEnd }
+TemplateDirective     = !{ TemplateIf | TemplateFor }
+
+// Interpolation expression start
+TemplateIExprStart       = _{ TemplateIExprStartStrip | TemplateIExprStartNormal }
+TemplateIExprStartNormal =  { "${" }
+TemplateIExprStartStrip  =  { "${~" }
+
+// Directive expression start
+TemplateDExprStart       = _{ TemplateDExprStartStrip | TemplateDExprStartNormal }
+TemplateDExprStartNormal =  { "%{" }
+TemplateDExprStartStrip  =  { "%{~" }
+
+// Interpolation or directive expression end
+TemplateExprEnd       = _{ TemplateExprEndNormal | TemplateExprEndStrip }
+TemplateExprEndNormal =  { "}" }
+TemplateExprEndStrip  =  { "~}" }
+
+// If directive
+TemplateIf        = { TemplateIfExpr ~ TemplateElseExpr? ~ TemplateEndIfExpr }
+TemplateIfExpr    = { TemplateDExprStart ~ "if" ~ Expression ~ TemplateExprEnd ~ Template }
+TemplateElseExpr  = { TemplateDExprStart ~ "else" ~ TemplateExprEnd ~ Template }
+TemplateEndIfExpr = { TemplateDExprStart ~ "endif" ~ TemplateExprEnd }
+
+// For directive
+TemplateFor        = { TemplateForExpr ~ TemplateEndForExpr }
+TemplateForExpr    = { TemplateDExprStart ~ "for" ~ Identifier ~ ("," ~ Identifier)? ~ "in" ~ Expression ~ TemplateExprEnd ~ Template }
+TemplateEndForExpr = { TemplateDExprStart ~ "endfor" ~ TemplateExprEnd }
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 79af27d..45d6d95 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -1,10 +1,10 @@
+mod template;
 #[cfg(test)]
 mod tests;
-mod unescape;
 
 use crate::{
-    Attribute, Block, BlockLabel, Body, Expression, Number, Object, ObjectKey, RawExpression,
-    Result, Structure,
+    structure::Identifier, util::unescape, Attribute, Block, BlockLabel, Body, Expression, Heredoc,
+    HeredocStripMode, Number, Object, ObjectKey, RawExpression, Result, Structure, TemplateExpr,
 };
 use pest::{
     iterators::{Pair, Pairs},
@@ -12,7 +12,7 @@ use pest::{
 };
 use pest_derive::Parser;
 use std::str::FromStr;
-use unescape::unescape;
+pub use template::parse as parse_template;
 
 #[derive(Parser)]
 #[grammar = "parser/grammar/hcl.pest"]
@@ -167,10 +167,13 @@ fn parse_expr_term(pair: Pair<Rule>) -> Result<Expression> {
         match pair.as_rule() {
             Rule::BooleanLit => Ok(Expression::Bool(parse_primitive(pair))),
             Rule::Float => Ok(Expression::Number(parse_primitive::<f64>(pair).into())),
-            Rule::Heredoc => parse_heredoc(pair).map(Expression::String),
             Rule::Int => Ok(Expression::Number(parse_primitive::<i64>(pair).into())),
             Rule::NullLit => Ok(Expression::Null),
             Rule::StringLit => parse_string(inner(pair)).map(Expression::String),
+            Rule::TemplateExpr => {
+                let expr = parse_template_expr(inner(pair));
+                Ok(Expression::TemplateExpr(Box::new(expr)))
+            }
             Rule::Tuple => parse_expressions(pair).map(Expression::Array),
             Rule::Object => parse_object(pair).map(Expression::Object),
             // @TODO(mohmann): Process ForExpr, VariableExpr etc.
@@ -179,6 +182,17 @@ fn parse_expr_term(pair: Pair<Rule>) -> Result<Expression> {
     }
 }
 
+fn parse_template_expr(pair: Pair<Rule>) -> TemplateExpr {
+    match pair.as_rule() {
+        Rule::QuotedStringTemplate => {
+            let pair = inner(pair);
+            TemplateExpr::QuotedString(pair.as_str().to_owned())
+        }
+        Rule::HeredocTemplate => TemplateExpr::Heredoc(parse_heredoc(pair)),
+        rule => unexpected_rule(rule),
+    }
+}
+
 fn parse_object(pair: Pair<Rule>) -> Result<Object<ObjectKey, Expression>> {
     ObjectIter::new(pair)
         .map(|(k, v)| Ok((parse_object_key(k)?, parse_expression(v)?)))
@@ -213,7 +227,7 @@ fn inner(pair: Pair<Rule>) -> Pair<Rule> {
 }
 
 fn parse_string(pair: Pair<Rule>) -> Result<String> {
-    unescape(pair.as_str())
+    unescape(pair.as_str()).map(|c| c.to_string())
 }
 
 fn parse_ident(pair: Pair<Rule>) -> String {
@@ -224,67 +238,23 @@ fn raw_expression(raw: &str) -> RawExpression {
     RawExpression::new(raw.trim_end())
 }
 
-fn parse_heredoc(pair: Pair<Rule>) -> Result<String> {
+fn parse_heredoc(pair: Pair<Rule>) -> Heredoc {
     let mut pairs = pair.into_inner();
     let intro = pairs.next().unwrap();
-    let content = pairs.nth(1).unwrap();
+    let delimiter = pairs.next().unwrap();
+    let template = pairs.next().unwrap();
 
-    match intro.as_rule() {
-        Rule::HeredocIntroNormal => parse_string(content),
-        Rule::HeredocIntroIndent => {
-            let dedented = dedent_string(content.as_str());
-            unescape(&dedented)
-        }
+    let strip = match intro.as_rule() {
+        Rule::HeredocIntroNormal => HeredocStripMode::None,
+        Rule::HeredocIntroIndent => HeredocStripMode::Indent,
         rule => unexpected_rule(rule),
-    }
-}
-
-// String dedent implementation which does not distinguish between spaces, tabs or unicode
-// whitespace but simply treats all of them as "one unit of whitespace".
-//
-// This is how the original HCL spec seems to handle it based on the original specsuite although it
-// is not formally defined. E.g. ' ' (space) and '\u{2003}' (unicode "em-space") are treated as one
-// unit of whitespace even though the former is 1 byte and the latter is 3 bytes long.
-fn dedent_string(s: &str) -> String {
-    if s.is_empty() {
-        return String::new();
-    }
+    };
 
-    let mut leading_ws = usize::MAX;
-    let mut non_empty_lines = 0;
-
-    // Find the minimum number of possible leading units of whitespace that can be be stripped off
-    // of each non-empty line.
-    for line in s.lines().filter(|line| !line.is_empty()) {
-        let line_leading_ws = line.chars().take_while(|ch| ch.is_whitespace()).count();
-
-        if line_leading_ws == 0 {
-            // Fast path: no dedent needed if we encounter a non-empty line which starts with a
-            // non-whitespace character.
-            return s.to_string();
-        }
-
-        leading_ws = leading_ws.min(line_leading_ws);
-        non_empty_lines += 1;
-    }
-
-    // Strip the determined amount of leading whitespace off of each line.
-    let mut dedented = String::with_capacity(s.len() - leading_ws * non_empty_lines);
-
-    for line in s.lines() {
-        if !line.is_empty() {
-            dedented.extend(line.chars().skip(leading_ws));
-        }
-
-        dedented.push('\n');
+    Heredoc {
+        strip,
+        delimiter: Identifier::new(delimiter.as_str()),
+        template: template.as_str().to_owned(),
     }
-
-    if dedented.ends_with('\n') && !s.ends_with('\n') {
-        let new_len = dedented.len() - 1;
-        dedented.truncate(new_len);
-    }
-
-    dedented
 }
 
 #[track_caller]
diff --git a/src/parser/template.rs b/src/parser/template.rs
new file mode 100644
index 0000000..a0d6c4a
--- /dev/null
+++ b/src/parser/template.rs
@@ -0,0 +1,165 @@
+use super::*;
+use crate::template::{
+    Directive, Element, ElseExpr, ForDirective, ForExpr, IfDirective, IfExpr, Interpolation,
+    StripMode, Template,
+};
+
+pub fn parse(input: &str) -> Result<Template> {
+    let pair = HclParser::parse(Rule::HclTemplate, input)?.next().unwrap();
+    parse_template(pair)
+}
+
+fn parse_template(pair: Pair<Rule>) -> Result<Template> {
+    pair.into_inner().map(parse_element).collect()
+}
+
+fn parse_element(pair: Pair<Rule>) -> Result<Element> {
+    match pair.as_rule() {
+        Rule::TemplateLiteral => Ok(Element::Literal(pair.as_str().to_owned())),
+        Rule::TemplateInterpolation => parse_interpolation(pair).map(Element::Interpolation),
+        Rule::TemplateDirective => parse_directive(pair).map(Element::Directive),
+        rule => unexpected_rule(rule),
+    }
+}
+
+fn parse_interpolation(pair: Pair<Rule>) -> Result<Interpolation> {
+    let mut pairs = pair.into_inner();
+    let start = pairs.next().unwrap();
+    let expr = pairs.next().unwrap();
+    let end = pairs.next().unwrap();
+
+    Ok(Interpolation {
+        expr: parse_expression(expr)?,
+        strip: parse_strip_mode(start, end),
+    })
+}
+
+fn parse_directive(pair: Pair<Rule>) -> Result<Directive> {
+    let pair = inner(pair);
+
+    match pair.as_rule() {
+        Rule::TemplateIf => parse_if_directive(pair).map(Directive::If),
+        Rule::TemplateFor => parse_for_directive(pair).map(Directive::For),
+        rule => unexpected_rule(rule),
+    }
+}
+
+fn parse_if_directive(pair: Pair<Rule>) -> Result<IfDirective> {
+    let mut pairs = pair.into_inner();
+    let expr = pairs.next().unwrap();
+    let if_expr = parse_if_expr(expr)?;
+
+    let mut expr = pairs.next().unwrap();
+
+    // Else branch is optional.
+    let else_expr = match expr.as_rule() {
+        Rule::TemplateElseExpr => {
+            let else_expr = parse_else_expr(expr)?;
+            expr = pairs.next().unwrap();
+            Some(else_expr)
+        }
+        Rule::TemplateEndIfExpr => None,
+        rule => unexpected_rule(rule),
+    };
+
+    Ok(IfDirective {
+        if_expr,
+        else_expr,
+        strip: parse_end_expr_strip_mode(expr),
+    })
+}
+
+fn parse_if_expr(pair: Pair<Rule>) -> Result<IfExpr> {
+    let mut pairs = pair.into_inner();
+    let start = pairs.next().unwrap();
+    let expr = pairs.next().unwrap();
+    let end = pairs.next().unwrap();
+    let template = pairs.next().unwrap();
+
+    Ok(IfExpr {
+        expr: parse_expression(expr)?,
+        strip: parse_strip_mode(start, end),
+        template: parse_template(template)?,
+    })
+}
+
+fn parse_else_expr(pair: Pair<Rule>) -> Result<ElseExpr> {
+    let mut pairs = pair.into_inner();
+    let start = pairs.next().unwrap();
+    let end = pairs.next().unwrap();
+    let template = pairs.next().unwrap();
+
+    Ok(ElseExpr {
+        strip: parse_strip_mode(start, end),
+        template: parse_template(template)?,
+    })
+}
+
+fn parse_for_directive(pair: Pair<Rule>) -> Result<ForDirective> {
+    let mut pairs = pair.into_inner();
+    let for_expr = pairs.next().unwrap();
+    let endfor_expr = pairs.next().unwrap();
+
+    Ok(ForDirective {
+        for_expr: parse_for_expr(for_expr)?,
+        strip: parse_end_expr_strip_mode(endfor_expr),
+    })
+}
+
+fn parse_for_expr(pair: Pair<Rule>) -> Result<ForExpr> {
+    let mut pairs = pair.into_inner();
+    let start = pairs.next().unwrap();
+    let value = pairs.next().unwrap();
+    let mut value = Some(Identifier::new(value.as_str()));
+    let mut expr = pairs.next().unwrap();
+
+    // If there are two identifiers, the first one is the key and the second one the value.
+    let key = match expr.as_rule() {
+        Rule::Identifier => {
+            let key = value.replace(Identifier::new(expr.as_str()));
+            expr = pairs.next().unwrap();
+            key
+        }
+        _ => None,
+    };
+
+    let end = pairs.next().unwrap();
+    let template = pairs.next().unwrap();
+
+    Ok(ForExpr {
+        key,
+        value: value.take().unwrap(),
+        expr: parse_expression(expr)?,
+        template: parse_template(template)?,
+        strip: parse_strip_mode(start, end),
+    })
+}
+
+fn parse_end_expr_strip_mode(pair: Pair<Rule>) -> StripMode {
+    let mut pairs = pair.into_inner();
+    let start = pairs.next().unwrap();
+    let end = pairs.next().unwrap();
+
+    parse_strip_mode(start, end)
+}
+
+fn parse_strip_mode(start: Pair<Rule>, end: Pair<Rule>) -> StripMode {
+    let strip_start = match start.as_rule() {
+        Rule::TemplateIExprStartStrip | Rule::TemplateDExprStartStrip => true,
+        Rule::TemplateIExprStartNormal | Rule::TemplateDExprStartNormal => false,
+        rule => unexpected_rule(rule),
+    };
+
+    let strip_end = match end.as_rule() {
+        Rule::TemplateExprEndStrip => true,
+        Rule::TemplateExprEndNormal => false,
+        rule => unexpected_rule(rule),
+    };
+
+    match (strip_start, strip_end) {
+        (true, true) => StripMode::Both,
+        (true, false) => StripMode::Start,
+        (false, true) => StripMode::End,
+        (false, false) => StripMode::None,
+    }
+}
diff --git a/src/parser/unescape.rs b/src/parser/unescape.rs
deleted file mode 100644
index 91f024a..0000000
--- a/src/parser/unescape.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-use crate::{Error, Result};
-use std::str::Chars;
-
-/// Takes in a string with backslash escapes written out with literal backslash characters and
-/// converts it to a string with the proper escaped characters.
-///
-/// ## Errors
-///
-/// Returns an error if an invalid or incomplete escape sequence or unicode code point is
-/// encountered.
-pub fn unescape(s: &str) -> Result<String> {
-    let mut buf = String::with_capacity(s.len());
-    let mut chars = s.chars();
-    let mut scratch = String::new();
-
-    while let Some(c) = chars.next() {
-        if c != '\\' {
-            buf.push(c);
-            continue;
-        }
-
-        let c = match chars.next() {
-            Some('\n') => continue,
-            Some('b') => '\u{0008}',
-            Some('f') => '\u{000C}',
-            Some('n') => '\n',
-            Some('r') => '\r',
-            Some('t') => '\t',
-            Some('\'') => '\'',
-            Some('\"') => '\"',
-            Some('\\') => '\\',
-            Some('u') => match unescape_unicode(&mut chars, &mut scratch) {
-                Some(c) => c,
-                None => return Err(Error::InvalidUnicodeCodePoint(scratch)),
-            },
-            Some(c) => return Err(Error::InvalidEscape(c)),
-            None => return Err(Error::Eof),
-        };
-
-        buf.push(c);
-    }
-
-    Ok(buf)
-}
-
-fn unescape_unicode(chars: &mut Chars<'_>, scratch: &mut String) -> Option<char> {
-    scratch.clear();
-
-    for _ in 0..4 {
-        scratch.push(chars.next()?);
-    }
-
-    char::from_u32(u32::from_str_radix(scratch, 16).ok()?)
-}
diff --git a/src/structure/de.rs b/src/structure/de.rs
index 1306f4d..e361282 100644
--- a/src/structure/de.rs
+++ b/src/structure/de.rs
@@ -1,7 +1,8 @@
 //! Deserialize impls for HCL structure types.
 
 use super::{
-    Attribute, Block, BlockLabel, Body, Expression, Identifier, ObjectKey, RawExpression, Structure,
+    Attribute, Block, BlockLabel, Body, Expression, Heredoc, HeredocStripMode, Identifier,
+    ObjectKey, RawExpression, Structure, TemplateExpr,
 };
 use crate::{Error, Number, Result};
 use serde::de::{self, value::MapAccessDeserializer, IntoDeserializer};
@@ -241,7 +242,6 @@ impl<'de> de::Deserializer<'de> for Expression {
         string bytes byte_buf option unit unit_struct newtype_struct seq
         tuple tuple_struct map struct identifier ignored_any
     }
-    impl_deserialize_enum!();
 
     fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
     where
@@ -259,8 +259,21 @@ impl<'de> de::Deserializer<'de> for Expression {
             Expression::Array(array) => visitor.visit_seq(array.into_deserializer()),
             Expression::Object(object) => visitor.visit_map(object.into_deserializer()),
             Expression::Raw(expr) => expr.into_deserializer().deserialize_any(visitor),
+            Expression::TemplateExpr(expr) => expr.into_deserializer().deserialize_any(visitor),
         }
     }
+
+    fn deserialize_enum<V>(
+        self,
+        _name: &'static str,
+        _variants: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        visitor.visit_enum(self)
+    }
 }
 
 impl VariantName for Expression {
@@ -273,10 +286,62 @@ impl VariantName for Expression {
             Expression::Array(_) => "Array",
             Expression::Object(_) => "Object",
             Expression::Raw(_) => "Raw",
+            Expression::TemplateExpr(_) => "TemplateExpr",
         }
     }
 }
 
+impl<'de> de::EnumAccess<'de> for Expression {
+    type Error = Error;
+    type Variant = Self;
+
+    fn variant_seed<T>(self, seed: T) -> Result<(T::Value, Self::Variant), Self::Error>
+    where
+        T: de::DeserializeSeed<'de>,
+    {
+        let variant_name = self.variant_name();
+
+        seed.deserialize(variant_name.into_deserializer())
+            .map(|variant| (variant, self))
+    }
+}
+
+impl<'de> de::VariantAccess<'de> for Expression {
+    type Error = Error;
+
+    fn unit_variant(self) -> Result<(), Self::Error> {
+        de::Deserialize::deserialize(self)
+    }
+
+    fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
+    where
+        T: de::DeserializeSeed<'de>,
+    {
+        match self {
+            Expression::TemplateExpr(expr) => seed.deserialize(expr.into_deserializer()),
+            value => seed.deserialize(value.into_deserializer()),
+        }
+    }
+
+    fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.into_deserializer().deserialize_seq(visitor)
+    }
+
+    fn struct_variant<V>(
+        self,
+        _fields: &'static [&'static str],
+        visitor: V,
+    ) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        self.into_deserializer().deserialize_map(visitor)
+    }
+}
+
 impl<'de> IntoDeserializer<'de, Error> for ObjectKey {
     type Deserializer = Self;
 
@@ -325,6 +390,140 @@ impl<'de> IntoDeserializer<'de, Error> for RawExpression {
     }
 }
 
+impl<'de> IntoDeserializer<'de, Error> for TemplateExpr {
+    type Deserializer = Self;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self
+    }
+}
+
+impl<'de> de::Deserializer<'de> for TemplateExpr {
+    type Error = Error;
+
+    forward_to_deserialize_any! {
+        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str
+        string bytes byte_buf option unit unit_struct newtype_struct seq
+        tuple tuple_struct map struct identifier ignored_any
+    }
+    impl_deserialize_enum!();
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self {
+            TemplateExpr::QuotedString(string) => visitor.visit_string(string),
+            TemplateExpr::Heredoc(heredoc) => heredoc.into_deserializer().deserialize_any(visitor),
+        }
+    }
+}
+
+impl VariantName for TemplateExpr {
+    fn variant_name(&self) -> &'static str {
+        match self {
+            TemplateExpr::QuotedString(_) => "QuotedString",
+            TemplateExpr::Heredoc(_) => "Heredoc",
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for Heredoc {
+    type Deserializer = MapAccessDeserializer<HeredocAccess>;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        MapAccessDeserializer::new(HeredocAccess::new(self))
+    }
+}
+
+pub struct HeredocAccess {
+    delimiter: Option<Identifier>,
+    template: Option<String>,
+    strip: Option<HeredocStripMode>,
+}
+
+impl HeredocAccess {
+    fn new(value: Heredoc) -> Self {
+        HeredocAccess {
+            delimiter: Some(value.delimiter),
+            template: Some(value.template),
+            strip: Some(value.strip),
+        }
+    }
+}
+
+impl<'de> de::MapAccess<'de> for HeredocAccess {
+    type Error = Error;
+
+    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
+    where
+        K: de::DeserializeSeed<'de>,
+    {
+        if self.delimiter.is_some() {
+            seed.deserialize("delimiter".into_deserializer()).map(Some)
+        } else if self.template.is_some() {
+            seed.deserialize("template".into_deserializer()).map(Some)
+        } else if self.strip.is_some() {
+            seed.deserialize("strip".into_deserializer()).map(Some)
+        } else {
+            Ok(None)
+        }
+    }
+
+    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
+    where
+        V: de::DeserializeSeed<'de>,
+    {
+        if let Some(delimiter) = self.delimiter.take() {
+            seed.deserialize(delimiter.into_deserializer())
+        } else if let Some(template) = self.template.take() {
+            seed.deserialize(template.into_deserializer())
+        } else if let Some(strip) = self.strip.take() {
+            seed.deserialize(strip.into_deserializer())
+        } else {
+            Err(de::Error::custom("invalid HCL heredoc"))
+        }
+    }
+}
+
+impl<'de> IntoDeserializer<'de, Error> for HeredocStripMode {
+    type Deserializer = Self;
+
+    fn into_deserializer(self) -> Self::Deserializer {
+        self
+    }
+}
+
+impl<'de> de::Deserializer<'de> for HeredocStripMode {
+    type Error = Error;
+
+    forward_to_deserialize_any! {
+        bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str
+        string bytes byte_buf option unit unit_struct newtype_struct seq
+        tuple tuple_struct map struct identifier ignored_any
+    }
+    impl_deserialize_enum!();
+
+    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+    where
+        V: de::Visitor<'de>,
+    {
+        match self {
+            HeredocStripMode::None => visitor.visit_str("None"),
+            HeredocStripMode::Indent => visitor.visit_str("Indent"),
+        }
+    }
+}
+
+impl VariantName for HeredocStripMode {
+    fn variant_name(&self) -> &'static str {
+        match self {
+            HeredocStripMode::None => "None",
+            HeredocStripMode::Indent => "Indent",
+        }
+    }
+}
+
 impl<'de> IntoDeserializer<'de, Error> for Identifier {
     type Deserializer = NewtypeStructDeserializer<String>;
 
diff --git a/src/structure/expression.rs b/src/structure/expression.rs
index eb1b525..b29131a 100644
--- a/src/structure/expression.rs
+++ b/src/structure/expression.rs
@@ -1,6 +1,6 @@
 //! Types to represent HCL attribute value expressions.
 
-use super::Identifier;
+use super::{Identifier, TemplateExpr};
 use crate::{Number, Value};
 use serde::{Deserialize, Serialize};
 use std::borrow::Cow;
@@ -21,12 +21,15 @@ pub enum Expression {
     Bool(bool),
     /// Represents a number, either integer or float.
     Number(Number),
-    /// Represents a string.
+    /// Represents a string that does not contain any template interpolations or template
+    /// directives.
     String(String),
     /// Represents array.
     Array(Vec<Expression>),
     /// Represents an object.
     Object(Object<ObjectKey, Expression>),
+    /// A quoted string or heredoc that embeds a program written in the template sub-language.
+    TemplateExpr(Box<TemplateExpr>),
     /// Represents a raw HCL expression. This includes any expression kind that does match any of
     /// the enum variants above. See [`RawExpression`] for more details.
     Raw(RawExpression),
@@ -41,6 +44,7 @@ impl From<Expression> for Value {
             Expression::String(s) => Value::String(s),
             Expression::Array(array) => array.into_iter().collect(),
             Expression::Object(object) => object.into_iter().collect(),
+            Expression::TemplateExpr(expr) => Value::String(expr.to_string()),
             Expression::Raw(raw) => Value::String(raw.into()),
         }
     }
@@ -156,6 +160,12 @@ impl From<RawExpression> for Expression {
     }
 }
 
+impl From<TemplateExpr> for Expression {
+    fn from(expr: TemplateExpr) -> Self {
+        Expression::TemplateExpr(Box::new(expr))
+    }
+}
+
 /// Represents an object key.
 #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
 #[serde(rename = "$hcl::object_key")]
@@ -232,9 +242,9 @@ impl Display for ObjectKey {
 
 /// A type that holds the value of a raw expression.
 ///
-/// As of now, anthing that is not a null value, a boolean, number, string, array or object is
-/// treated as raw expression and is not further parsed. This includes conditionals, operations,
-/// function calls, for expressions and variable expressions.
+/// As of now, anthing that is not a null value, a boolean, number, string, template expression,
+/// array or object is treated as raw expression and is not further parsed. This includes
+/// conditionals, operations, function calls, for expressions and variable expressions.
 #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
 #[serde(rename = "$hcl::raw_expression")]
 pub struct RawExpression(String);
diff --git a/src/structure/mod.rs b/src/structure/mod.rs
index e4c856c..0c939a2 100644
--- a/src/structure/mod.rs
+++ b/src/structure/mod.rs
@@ -51,6 +51,7 @@ mod body;
 pub(crate) mod de;
 mod expression;
 pub(crate) mod ser;
+mod template;
 #[cfg(test)]
 mod tests;
 
@@ -59,12 +60,13 @@ pub use self::{
     block::{Block, BlockBuilder, BlockLabel},
     body::{Body, BodyBuilder},
     expression::{Expression, Object, ObjectKey, RawExpression},
+    template::{Heredoc, HeredocStripMode, TemplateExpr},
 };
 use crate::{Map, Value};
 use serde::{Deserialize, Serialize};
 use std::borrow::Cow;
 
-/// Represents an HCL identifier inside of a [`BlockLabel`] or [`ObjectKey`].
+/// Represents an HCL identifier.
 #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash)]
 #[serde(rename = "$hcl::identifier")]
 pub struct Identifier(pub String);
diff --git a/src/structure/ser/expression.rs b/src/structure/ser/expression.rs
index 1f32d44..2fe8684 100644
--- a/src/structure/ser/expression.rs
+++ b/src/structure/ser/expression.rs
@@ -1,4 +1,4 @@
-use super::StringSerializer;
+use super::{template::TemplateExprSerializer, StringSerializer};
 use crate::{serialize_unsupported, Error, Expression, Object, ObjectKey, RawExpression, Result};
 use serde::ser::{self, Impossible};
 use std::fmt::Display;
@@ -113,7 +113,7 @@ impl ser::Serializer for ExpressionSerializer {
     fn serialize_newtype_variant<T>(
         self,
         name: &'static str,
-        _variant_index: u32,
+        variant_index: u32,
         variant: &'static str,
         value: &T,
     ) -> Result<Self::Ok>
@@ -122,6 +122,15 @@ impl ser::Serializer for ExpressionSerializer {
     {
         if name == "$hcl::expression" {
             value.serialize(self)
+        } else if name == "$hcl::template_expr" {
+            Ok(Expression::TemplateExpr(Box::new(
+                TemplateExprSerializer.serialize_newtype_variant(
+                    name,
+                    variant_index,
+                    variant,
+                    value,
+                )?,
+            )))
         } else {
             let mut object = Object::new();
             object.insert(ObjectKey::identifier(variant), value.serialize(self)?);
diff --git a/src/structure/ser/mod.rs b/src/structure/ser/mod.rs
index b6ed1cb..734b124 100644
--- a/src/structure/ser/mod.rs
+++ b/src/structure/ser/mod.rs
@@ -5,6 +5,7 @@ mod block;
 pub(crate) mod body;
 mod expression;
 mod structure;
+mod template;
 #[cfg(test)]
 mod tests;
 
diff --git a/src/structure/ser/template.rs b/src/structure/ser/template.rs
new file mode 100644
index 0000000..8766084
--- /dev/null
+++ b/src/structure/ser/template.rs
@@ -0,0 +1,329 @@
+use super::StringSerializer;
+use crate::{
+    serialize_unsupported, structure::Identifier, Error, Heredoc, HeredocStripMode, Result,
+    TemplateExpr,
+};
+use serde::ser::{self, Impossible, Serialize};
+use std::fmt::Display;
+use std::str::FromStr;
+
+pub struct TemplateExprSerializer;
+
+impl ser::Serializer for TemplateExprSerializer {
+    type Ok = TemplateExpr;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<TemplateExpr, Error>;
+    type SerializeTuple = Impossible<TemplateExpr, Error>;
+    type SerializeTupleStruct = Impossible<TemplateExpr, Error>;
+    type SerializeTupleVariant = Impossible<TemplateExpr, Error>;
+    type SerializeMap = Impossible<TemplateExpr, Error>;
+    type SerializeStruct = Impossible<TemplateExpr, Error>;
+    type SerializeStructVariant = Impossible<TemplateExpr, Error>;
+
+    serialize_unsupported! {
+        i8 i16 i32 i64 u8 u16 u32 u64
+        bool f32 f64 bytes unit unit_struct none
+        seq tuple tuple_struct tuple_variant map struct struct_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_char(self, value: char) -> Result<Self::Ok> {
+        Ok(TemplateExpr::QuotedString(value.to_string()))
+    }
+
+    fn serialize_str(self, value: &str) -> Result<Self::Ok> {
+        Ok(TemplateExpr::QuotedString(value.to_owned()))
+    }
+
+    fn serialize_unit_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+    ) -> Result<Self::Ok> {
+        Ok(TemplateExpr::QuotedString(variant.to_owned()))
+    }
+
+    fn serialize_newtype_variant<T>(
+        self,
+        name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+        value: &T,
+    ) -> Result<Self::Ok>
+    where
+        T: ?Sized + Serialize,
+    {
+        // Specialization for the `TemplateExpr` type itself.
+        match (name, variant) {
+            ("$hcl::template_expr", "QuotedString") => Ok(TemplateExpr::QuotedString(
+                value.serialize(StringSerializer)?,
+            )),
+            ("$hcl::template_expr", "Heredoc") => {
+                Ok(TemplateExpr::Heredoc(value.serialize(HeredocSerializer)?))
+            }
+            (_, _) => value.serialize(self),
+        }
+    }
+
+    fn collect_str<T>(self, value: &T) -> Result<Self::Ok>
+    where
+        T: ?Sized + Display,
+    {
+        Ok(TemplateExpr::QuotedString(value.to_string()))
+    }
+}
+
+pub struct HeredocSerializer;
+
+impl ser::Serializer for HeredocSerializer {
+    type Ok = Heredoc;
+    type Error = Error;
+
+    type SerializeSeq = SerializeHeredocSeq;
+    type SerializeTuple = SerializeHeredocSeq;
+    type SerializeTupleStruct = SerializeHeredocSeq;
+    type SerializeTupleVariant = Impossible<Heredoc, Error>;
+    type SerializeMap = SerializeHeredocMap;
+    type SerializeStruct = SerializeHeredocStruct;
+    type SerializeStructVariant = Impossible<Heredoc, Error>;
+
+    serialize_unsupported! {
+        bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64
+        char str bytes none unit unit_struct unit_variant
+        tuple_variant struct_variant
+    }
+    serialize_self! { some newtype_struct }
+    forward_to_serialize_seq! { tuple tuple_struct }
+
+    fn serialize_newtype_variant<T>(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+        value: &T,
+    ) -> Result<Heredoc>
+    where
+        T: ?Sized + Serialize,
+    {
+        Ok(Heredoc {
+            delimiter: Identifier::new(variant),
+            template: value.serialize(StringSerializer)?,
+            strip: HeredocStripMode::None,
+        })
+    }
+
+    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
+        Ok(SerializeHeredocSeq::new())
+    }
+
+    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
+        Ok(SerializeHeredocMap::new())
+    }
+
+    fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> {
+        Ok(SerializeHeredocStruct::new())
+    }
+}
+
+pub struct SerializeHeredocSeq {
+    delimiter: Option<Identifier>,
+    template: Option<String>,
+    strip: Option<HeredocStripMode>,
+}
+
+impl SerializeHeredocSeq {
+    pub fn new() -> Self {
+        SerializeHeredocSeq {
+            delimiter: None,
+            template: None,
+            strip: None,
+        }
+    }
+}
+
+impl ser::SerializeSeq for SerializeHeredocSeq {
+    type Ok = Heredoc;
+    type Error = Error;
+
+    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        if self.delimiter.is_none() {
+            self.delimiter = Some(Identifier::from(value.serialize(StringSerializer)?));
+        } else if self.template.is_none() {
+            self.template = Some(value.serialize(StringSerializer)?);
+        } else if self.strip.is_none() {
+            self.strip = Some(value.serialize(HeredocStripModeSerializer)?);
+        } else {
+            return Err(ser::Error::custom("expected sequence with 2 or 3 elements"));
+        }
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.delimiter, self.template) {
+            (Some(delimiter), Some(template)) => Ok(Heredoc {
+                delimiter,
+                template,
+                strip: self.strip.unwrap_or(HeredocStripMode::None),
+            }),
+            (_, _) => Err(ser::Error::custom("expected sequence with 2 or 3 elements")),
+        }
+    }
+}
+
+impl ser::SerializeTuple for SerializeHeredocSeq {
+    impl_forward_to_serialize_seq!(serialize_element, Heredoc);
+}
+
+impl serde::ser::SerializeTupleStruct for SerializeHeredocSeq {
+    impl_forward_to_serialize_seq!(serialize_field, Heredoc);
+}
+
+pub struct SerializeHeredocMap {
+    delimiter: Option<Identifier>,
+    template: Option<String>,
+}
+
+impl SerializeHeredocMap {
+    pub fn new() -> Self {
+        SerializeHeredocMap {
+            delimiter: None,
+            template: None,
+        }
+    }
+}
+
+impl ser::SerializeMap for SerializeHeredocMap {
+    type Ok = Heredoc;
+    type Error = Error;
+
+    fn serialize_key<T>(&mut self, key: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        if self.delimiter.is_none() {
+            self.delimiter = Some(Identifier::from(key.serialize(StringSerializer)?));
+            Ok(())
+        } else {
+            Err(ser::Error::custom("expected map with 1 entry"))
+        }
+    }
+
+    fn serialize_value<T>(&mut self, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        if self.template.is_none() {
+            panic!("serialize_value called before serialize_key");
+        }
+
+        self.template = Some(value.serialize(StringSerializer)?);
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.delimiter, self.template) {
+            (Some(delimiter), Some(template)) => Ok(Heredoc {
+                delimiter,
+                template,
+                strip: HeredocStripMode::None,
+            }),
+            (_, _) => Err(ser::Error::custom("expected map with 1 entry")),
+        }
+    }
+}
+
+pub struct SerializeHeredocStruct {
+    delimiter: Option<Identifier>,
+    template: Option<String>,
+    strip: Option<HeredocStripMode>,
+}
+
+impl SerializeHeredocStruct {
+    pub fn new() -> Self {
+        SerializeHeredocStruct {
+            delimiter: None,
+            template: None,
+            strip: None,
+        }
+    }
+}
+
+impl ser::SerializeStruct for SerializeHeredocStruct {
+    type Ok = Heredoc;
+    type Error = Error;
+
+    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
+    where
+        T: ?Sized + ser::Serialize,
+    {
+        match key {
+            "delimiter" => {
+                self.delimiter = Some(Identifier::from(value.serialize(StringSerializer)?))
+            }
+            "template" => self.template = Some(value.serialize(StringSerializer)?),
+            "strip" => self.strip = Some(value.serialize(HeredocStripModeSerializer)?),
+            _ => {
+                return Err(ser::Error::custom(
+                    "expected struct with fields `delimiter`, `template` and optional `strip`",
+                ))
+            }
+        };
+
+        Ok(())
+    }
+
+    fn end(self) -> Result<Self::Ok> {
+        match (self.delimiter, self.template) {
+            (Some(delimiter), Some(template)) => Ok(Heredoc {
+                delimiter,
+                template,
+                strip: self.strip.unwrap_or(HeredocStripMode::None),
+            }),
+            (_, _) => Err(ser::Error::custom(
+                "expected struct with fields `delimiter`, `template` and optional `strip`",
+            )),
+        }
+    }
+}
+
+pub struct HeredocStripModeSerializer;
+
+impl ser::Serializer for HeredocStripModeSerializer {
+    type Ok = HeredocStripMode;
+    type Error = Error;
+
+    type SerializeSeq = Impossible<HeredocStripMode, Error>;
+    type SerializeTuple = Impossible<HeredocStripMode, Error>;
+    type SerializeTupleStruct = Impossible<HeredocStripMode, Error>;
+    type SerializeTupleVariant = Impossible<HeredocStripMode, Error>;
+    type SerializeMap = Impossible<HeredocStripMode, Error>;
+    type SerializeStruct = Impossible<HeredocStripMode, Error>;
+    type SerializeStructVariant = Impossible<HeredocStripMode, Error>;
+
+    serialize_unsupported! {
+        i8 i16 i32 i64 u8 u16 u32 u64
+        bool f32 f64 char bytes unit unit_struct none
+        seq tuple tuple_struct tuple_variant map struct struct_variant
+        newtype_variant
+    }
+    serialize_self! { some newtype_struct }
+
+    fn serialize_str(self, value: &str) -> Result<Self::Ok> {
+        HeredocStripMode::from_str(value)
+    }
+
+    fn serialize_unit_variant(
+        self,
+        _name: &'static str,
+        _variant_index: u32,
+        variant: &'static str,
+    ) -> Result<Self::Ok> {
+        self.serialize_str(variant)
+    }
+}
diff --git a/src/structure/template.rs b/src/structure/template.rs
new file mode 100644
index 0000000..5cccce5
--- /dev/null
+++ b/src/structure/template.rs
@@ -0,0 +1,138 @@
+use super::Identifier;
+use crate::{
+    util::{dedent, try_unescape},
+    Error, Result,
+};
+use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
+use std::{fmt, str::FromStr};
+
+/// A template expression embeds a program written in the template sub-language as an expression.
+///
+/// This type wraps the raw template string representation. Refer to the documentation of the
+/// [`template`][`crate::template`] module if you need to parse and further evaluate the raw
+/// template.
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+#[serde(rename = "$hcl::template_expr")]
+pub enum TemplateExpr {
+    /// A quoted template expression is delimited by quote characters (`"`) and defines a template
+    /// as a single-line expression with escape characters. The raw template string may contain
+    /// escape sequences.
+    QuotedString(String),
+    /// A heredoc template expression is introduced by a `<<` sequence and defines a template via a
+    /// multi-line sequence terminated by a user-chosen delimiter. The raw template string in the
+    /// heredoc may contain escape sequences.
+    Heredoc(Heredoc),
+}
+
+impl TemplateExpr {
+    /// Returns the template as a copy-on-write string.
+    pub(crate) fn to_cow_str(&self) -> Cow<str> {
+        match self {
+            TemplateExpr::QuotedString(s) => Cow::Borrowed(s),
+            TemplateExpr::Heredoc(heredoc) => heredoc.to_cow_str(),
+        }
+    }
+}
+
+impl From<String> for TemplateExpr {
+    fn from(string: String) -> Self {
+        TemplateExpr::QuotedString(string)
+    }
+}
+
+impl From<Heredoc> for TemplateExpr {
+    fn from(heredoc: Heredoc) -> Self {
+        TemplateExpr::Heredoc(heredoc)
+    }
+}
+
+impl fmt::Display for TemplateExpr {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let s = self.to_cow_str();
+        f.write_str(&try_unescape(&s))
+    }
+}
+
+/// A heredoc template expression is introduced by a `<<` sequence and defines a template via a
+/// multi-line sequence terminated by a user-chosen delimiter.
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+#[serde(rename = "$hcl::heredoc")]
+pub struct Heredoc {
+    /// The delimiter identifier that denotes the heredoc start and end.
+    pub delimiter: Identifier,
+    /// The raw template contained in the heredoc.
+    pub template: String,
+    /// The heredoc strip mode.
+    pub strip: HeredocStripMode,
+}
+
+impl Heredoc {
+    /// Creates a new `Heredoc` with the provided delimiter and template body.
+    pub fn new<T>(delimiter: Identifier, template: T) -> Heredoc
+    where
+        T: Into<String>,
+    {
+        Heredoc {
+            delimiter,
+            template: template.into(),
+            strip: HeredocStripMode::default(),
+        }
+    }
+
+    /// Sets the heredoc strip mode to use on the template.
+    pub fn with_strip_mode(mut self, strip: HeredocStripMode) -> Heredoc {
+        self.strip = strip;
+        self
+    }
+
+    /// Returns the template as a copy-on-write string.
+    pub(crate) fn to_cow_str(&self) -> Cow<str> {
+        match self.strip {
+            HeredocStripMode::None => Cow::Borrowed(&self.template),
+            HeredocStripMode::Indent => dedent(&self.template),
+        }
+    }
+}
+
+/// The strip behaviour for the template contained in the heredoc.
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
+#[serde(rename = "$hcl::heredoc_strip")]
+pub enum HeredocStripMode {
+    /// Do not strip leading whitespace.
+    None,
+    /// Any literal string at the start of each line is analyzed to find the minimum number
+    /// of leading spaces, and then that number of prefix spaces is removed from all line-leading
+    /// literal strings. The final closing marker may also have an arbitrary number of spaces
+    /// preceding it on its line.
+    Indent,
+}
+
+impl HeredocStripMode {
+    /// Returns the string representation of the heredoc strip mode. This is the part before the
+    /// delimiter identifier.
+    pub fn as_str(&self) -> &'static str {
+        match self {
+            HeredocStripMode::None => "<<",
+            HeredocStripMode::Indent => "<<-",
+        }
+    }
+}
+
+impl Default for HeredocStripMode {
+    fn default() -> Self {
+        HeredocStripMode::None
+    }
+}
+
+impl FromStr for HeredocStripMode {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self> {
+        match s {
+            "None" | "none" | "<<" => Ok(HeredocStripMode::None),
+            "Indent" | "indent" | "<<-" => Ok(HeredocStripMode::Indent),
+            _ => Err(Error::new(format!("invalid heredoc strip mode: `{}`", s))),
+        }
+    }
+}
diff --git a/src/template.rs b/src/template.rs
new file mode 100644
index 0000000..32cbf5a
--- /dev/null
+++ b/src/template.rs
@@ -0,0 +1,497 @@
+//! This module implements the HCL template sub-language.
+//!
+//! When parsing an HCL document, template expressions are emitted as
+//! [`TemplateExpr`][`crate::structure::TemplateExpr`] (as the `TemplateExpr` variant of the
+//! [`Expression`][`crate::structure::Expression`] enum) which contain the raw unparsed template
+//! strings.
+//!
+//! These template expression can be further parsed into a [`Template`] which is composed literal
+//! strings, template interpolations and template directives.
+//!
+//! Refer to the [HCL syntax specification][hcl-syntax-spec] for the detail.
+//!
+//! [hcl-syntax-spec]: https://github.com/hashicorp/hcl/blob/main/hclsyntax/spec.md#templates
+//!
+//! ## Example
+//!
+//! Parse a `TemplateExpr` into a `Template`:
+//!
+//! ```
+//! # use std::error::Error;
+//! #
+//! # fn main() -> Result<(), Box<dyn Error>> {
+//! use hcl::{template::{Interpolation, Template}};
+//! use hcl::{RawExpression, TemplateExpr};
+//!
+//! let expr = TemplateExpr::QuotedString(String::from("Hello ${name}!"));
+//! let template = Template::from_expr(&expr)?;
+//!
+//! let expected = Template::new()
+//!     .add_literal("Hello ")
+//!     .add_interpolation(Interpolation::new(RawExpression::new("name")))
+//!     .add_literal("!");
+//!
+//! assert_eq!(expected, template);
+//! #   Ok(())
+//! # }
+//! ```
+//!
+//! It is also possible to use the template sub-language in a standalone way by parsing template
+//! strings directly:
+//!
+//! ```
+//! # use std::error::Error;
+//! #
+//! # fn main() -> Result<(), Box<dyn Error>> {
+//! use hcl::{template::{ForDirective, ForExpr, Interpolation, StripMode, Template}};
+//! use hcl::{Identifier, RawExpression};
+//! use std::str::FromStr;
+//!
+//! let raw = r#"
+//! Bill of materials:
+//! %{ for item in items ~}
+//! - ${item}
+//! %{ endfor ~}
+//! "#;
+//!
+//! let template = Template::from_str(raw)?;
+//!
+//! let expected = Template::new()
+//!     .add_literal("Bill of materials:\n")
+//!     .add_directive(
+//!         ForDirective::new(
+//!             ForExpr::new(
+//!                 Identifier::new("item"),
+//!                 RawExpression::new("items"),
+//!                 Template::new()
+//!                     .add_literal("- ")
+//!                     .add_interpolation(
+//!                         Interpolation::new(RawExpression::new("item"))
+//!                     )
+//!                     .add_literal("\n")
+//!             )
+//!             .with_strip_mode(StripMode::End)
+//!         )
+//!         .with_strip_mode(StripMode::End)
+//!     )
+//!     .add_literal("\n");
+//!
+//! assert_eq!(expected, template);
+//! #   Ok(())
+//! # }
+//! ```
+
+use crate::{parser, structure::Identifier, Error, Expression, Result, TemplateExpr};
+use std::str::FromStr;
+
+/// A template behaves like an expression that always returns a string value. The different
+/// elements of the template are evaluated and combined into a single string to return.
+///
+/// See the [`module level`][`crate::template`] documentation for usage examples.
+#[derive(Debug, Clone, PartialEq, Default)]
+pub struct Template {
+    elements: Vec<Element>,
+}
+
+impl Template {
+    /// Creates an empty template with no elements.
+    pub fn new() -> Template {
+        Template {
+            elements: Vec::new(),
+        }
+    }
+
+    /// Expands a raw template expression to a template.
+    ///
+    /// ## Errors
+    ///
+    /// Returns an error if the parsing of raw string templates fails or if the template expression
+    /// contains string literals with invalid escape sequences.
+    pub fn from_expr(expr: &TemplateExpr) -> Result<Self> {
+        Template::from_str(&expr.to_cow_str())
+    }
+
+    /// Returns a reference to the template elements.
+    pub fn elements(&self) -> &[Element] {
+        &self.elements
+    }
+
+    /// Returns a mutable reference to the template elements.
+    pub fn elements_mut(&mut self) -> &mut [Element] {
+        &mut self.elements
+    }
+}
+
+// Builder methods.
+impl Template {
+    /// Adds a template element (literal, interpolation or directive) to the template.
+    pub fn add_element<T>(mut self, element: T) -> Template
+    where
+        T: Into<Element>,
+    {
+        self.elements.push(element.into());
+        self
+    }
+
+    /// Adds a literal to the template.
+    pub fn add_literal<T>(self, literal: T) -> Template
+    where
+        T: Into<String>,
+    {
+        self.add_element(literal.into())
+    }
+
+    /// Adds an interpolation to the template.
+    pub fn add_interpolation<T>(self, interpolation: T) -> Template
+    where
+        T: Into<Interpolation>,
+    {
+        self.add_element(interpolation.into())
+    }
+
+    /// Adds a directive to the template.
+    pub fn add_directive<T>(self, directive: T) -> Template
+    where
+        T: Into<Directive>,
+    {
+        self.add_element(directive.into())
+    }
+}
+
+impl FromStr for Template {
+    type Err = Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        parser::parse_template(s)
+    }
+}
+
+impl<T> FromIterator<T> for Template
+where
+    T: Into<Element>,
+{
+    fn from_iter<I>(iter: I) -> Self
+    where
+        I: IntoIterator<Item = T>,
+    {
+        Template {
+            elements: iter.into_iter().map(Into::into).collect(),
+        }
+    }
+}
+
+/// An element of an HCL template.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Element {
+    /// A literal sequence of characters to include in the resulting string.
+    Literal(String),
+    /// An interpolation sequence that evaluates an expression (written in the expression
+    /// sub-language), and converts the result to a string value.
+    Interpolation(Interpolation),
+    /// A `if` and `for` directive that allows for conditional template evaluation.
+    Directive(Directive),
+}
+
+impl From<&str> for Element {
+    fn from(literal: &str) -> Self {
+        Element::Literal(literal.to_owned())
+    }
+}
+
+impl From<String> for Element {
+    fn from(literal: String) -> Self {
+        Element::Literal(literal)
+    }
+}
+
+impl From<Interpolation> for Element {
+    fn from(interpolation: Interpolation) -> Self {
+        Element::Interpolation(interpolation)
+    }
+}
+
+impl From<Directive> for Element {
+    fn from(directive: Directive) -> Self {
+        Element::Directive(directive)
+    }
+}
+
+/// An interpolation sequence evaluates an expression (written in the expression sub-language),
+/// converts the result to a string value, and replaces itself with the resulting string.
+#[derive(Debug, Clone, PartialEq)]
+pub struct Interpolation {
+    /// The interpolated expression.
+    pub expr: Expression,
+    /// The whitespace strip mode to use on the template elements preceeding and following after
+    /// this interpolation sequence.
+    pub strip: StripMode,
+}
+
+impl Interpolation {
+    /// Creates a new expression `Interpolation`.
+    pub fn new<T>(expr: T) -> Interpolation
+    where
+        T: Into<Expression>,
+    {
+        Interpolation {
+            expr: expr.into(),
+            strip: StripMode::None,
+        }
+    }
+
+    /// Sets the whitespace strip mode to use on the template elements preceeding and following
+    /// after this interpolation sequence and returns the modified `Interpolation`.
+    pub fn with_strip_mode(mut self, strip: StripMode) -> Interpolation {
+        self.strip = strip;
+        self
+    }
+}
+
+impl From<Expression> for Interpolation {
+    fn from(expr: Expression) -> Self {
+        Interpolation {
+            expr,
+            strip: StripMode::default(),
+        }
+    }
+}
+
+/// A template directive that allows for conditional template evaluation.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Directive {
+    /// Represents a template `if` directive.
+    If(IfDirective),
+    /// Represents a template `for` directive.
+    For(ForDirective),
+}
+
+impl From<IfDirective> for Directive {
+    fn from(directive: IfDirective) -> Self {
+        Directive::If(directive)
+    }
+}
+
+impl From<ForDirective> for Directive {
+    fn from(directive: ForDirective) -> Self {
+        Directive::For(directive)
+    }
+}
+
+/// The template `if` directive is the template equivalent of the conditional expression, allowing
+/// selection of one of two sub-templates based on the value of a predicate expression.
+#[derive(Debug, Clone, PartialEq)]
+pub struct IfDirective {
+    /// The `if` branch expression.
+    pub if_expr: IfExpr,
+    /// The optional `else` branch expression.
+    pub else_expr: Option<ElseExpr>,
+    /// The whitespace strip mode to use on the template elements preceeding and following after
+    /// the `endif` marker of this directive.
+    pub strip: StripMode,
+}
+
+impl IfDirective {
+    /// Creates a new `IfDirective` from an `if` expression.
+    pub fn new<T>(if_expr: T) -> IfDirective
+    where
+        T: Into<IfExpr>,
+    {
+        IfDirective {
+            if_expr: if_expr.into(),
+            else_expr: None,
+            strip: StripMode::default(),
+        }
+    }
+
+    /// Adds an `else` expression and returns the modified `IfDirective`.
+    pub fn with_else_expr<T>(mut self, else_expr: T) -> IfDirective
+    where
+        T: Into<ElseExpr>,
+    {
+        self.else_expr = Some(else_expr.into());
+        self
+    }
+
+    /// Sets the whitespace strip mode to use on the template elements preceeding and following
+    /// after the `endif` marker of this directive and returns the modified `IfDirective`.
+    pub fn with_strip_mode(mut self, strip: StripMode) -> IfDirective {
+        self.strip = strip;
+        self
+    }
+}
+
+impl From<IfExpr> for IfDirective {
+    fn from(if_expr: IfExpr) -> Self {
+        IfDirective::new(if_expr)
+    }
+}
+
+/// The `if` branch of an `if` directive.
+#[derive(Debug, Clone, PartialEq)]
+pub struct IfExpr {
+    /// The conditional expression.
+    pub expr: Expression,
+    /// The template that is included in the result string if the conditional expression evaluates
+    /// to `true`.
+    pub template: Template,
+    /// The whitespace strip mode to use on the template elements preceeding and following after
+    /// the `if` expression.
+    pub strip: StripMode,
+}
+
+impl IfExpr {
+    /// Creates a new `IfExpr` from an expression and a template that is included in the result
+    /// string if the conditional expression evaluates to `true`.
+    pub fn new<T>(expr: T, template: Template) -> IfExpr
+    where
+        T: Into<Expression>,
+    {
+        IfExpr {
+            expr: expr.into(),
+            template,
+            strip: StripMode::default(),
+        }
+    }
+
+    /// Sets the whitespace strip mode to use on the template elements preceeding and following
+    /// after the `if` expression and returns the modified `IfExpr`.
+    pub fn with_strip_mode(mut self, strip: StripMode) -> IfExpr {
+        self.strip = strip;
+        self
+    }
+}
+
+/// The `else` branch expression of an `if` directive.
+#[derive(Debug, Clone, PartialEq)]
+pub struct ElseExpr {
+    /// The template that is included in the result string if the `if` branch's conditional
+    /// expression evaluates to `false`.
+    pub template: Template,
+    /// The whitespace strip mode to use on the template elements preceeding and following after
+    /// the `else` expression.
+    pub strip: StripMode,
+}
+
+impl ElseExpr {
+    /// Creates a new `ElseExpr` from a template that is included in the result string if the `if`
+    /// branch's conditional expression evaluates to `false`.
+    pub fn new(template: Template) -> ElseExpr {
+        ElseExpr {
+            template,
+            strip: StripMode::default(),
+        }
+    }
+
+    /// Sets the whitespace strip mode to use on the template elements preceeding and following
+    /// after the `else` expression and returns the modified `ElseExpr`.
+    pub fn with_strip_mode(mut self, strip: StripMode) -> ElseExpr {
+        self.strip = strip;
+        self
+    }
+}
+
+/// The template `for` directive is the template equivalent of the for expression, producing zero
+/// or more copies of its sub-template based on the elements of a collection.
+#[derive(Debug, Clone, PartialEq)]
+pub struct ForDirective {
+    /// The loop expression.
+    pub for_expr: ForExpr,
+    /// The whitespace strip mode to use on the template elements preceeding and following after
+    /// the `endfor` marker of this directive.
+    pub strip: StripMode,
+}
+
+impl ForDirective {
+    /// Creates a new `ForDirective` from a `for` expression.
+    pub fn new<T>(for_expr: T) -> ForDirective
+    where
+        T: Into<ForExpr>,
+    {
+        ForDirective {
+            for_expr: for_expr.into(),
+            strip: StripMode::default(),
+        }
+    }
+
+    /// Sets the whitespace strip mode to use on the template elements preceeding and following
+    /// after the `endfor` marker of this directive and returns the modified `ForDirective`.
+    pub fn with_strip_mode(mut self, strip: StripMode) -> ForDirective {
+        self.strip = strip;
+        self
+    }
+}
+
+impl From<ForExpr> for ForDirective {
+    fn from(for_expr: ForExpr) -> Self {
+        ForDirective::new(for_expr)
+    }
+}
+
+/// The `for` expression header of a `for` directive.
+#[derive(Debug, Clone, PartialEq)]
+pub struct ForExpr {
+    /// Optional iterator key variable identifier.
+    pub key: Option<Identifier>,
+    /// The iterator value variable identifier.
+    pub value: Identifier,
+    /// The expression that produces the list or object of elements to iterate over.
+    pub expr: Expression,
+    /// The template that is included in the result string for each loop iteration.
+    pub template: Template,
+    /// The whitespace strip mode to use on the template elements preceeding and following after
+    /// the `for` expression.
+    pub strip: StripMode,
+}
+
+impl ForExpr {
+    /// Creates a new `ForExpr` from the provided iterator value identifier, an expression that
+    /// produces the list or object of elements to iterate over, and the template the is included
+    /// in the result string for each loop iteration.
+    pub fn new<T>(value: Identifier, expr: T, template: Template) -> ForExpr
+    where
+        T: Into<Expression>,
+    {
+        ForExpr {
+            key: None,
+            value,
+            expr: expr.into(),
+            template,
+            strip: StripMode::default(),
+        }
+    }
+
+    /// Adds the iterator key variable identifier to the `for` expression and returns the modified
+    /// `ForExpr`.
+    pub fn with_key(mut self, key: Identifier) -> ForExpr {
+        self.key = Some(key);
+        self
+    }
+
+    /// Sets the whitespace strip mode to use on the template elements preceeding and following
+    /// after the `for` expression and returns the modified `ForExpr`.
+    pub fn with_strip_mode(mut self, strip: StripMode) -> ForExpr {
+        self.strip = strip;
+        self
+    }
+}
+
+/// Controls the whitespace strip behaviour on adjacent string literals.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum StripMode {
+    /// Don't strip adjacent spaces.
+    None,
+    /// Strip any adjacent spaces from the immediately preceeding string literal, if there is
+    /// one.
+    Start,
+    /// Strip any adjacent spaces from the immediately following string literal, if there is one.
+    End,
+    /// Strip any adjacent spaces from the immediately preceeding and following string literals,
+    /// if there are any.
+    Both,
+}
+
+impl Default for StripMode {
+    fn default() -> StripMode {
+        StripMode::None
+    }
+}
diff --git a/src/util.rs b/src/util.rs
new file mode 100644
index 0000000..4d61ea5
--- /dev/null
+++ b/src/util.rs
@@ -0,0 +1,128 @@
+use crate::{Error, Result};
+use std::borrow::Cow;
+use std::str::Chars;
+
+// String dedent implementation which does not distinguish between spaces, tabs or unicode
+// whitespace but simply treats all of them as "one unit of whitespace".
+//
+// This is how the original HCL spec seems to handle it based on the original specsuite although it
+// is not formally defined. E.g. ' ' (space) and '\u{2003}' (unicode "em-space") are treated as one
+// unit of whitespace even though the former is 1 byte and the latter is 3 bytes long.
+pub fn dedent(s: &str) -> Cow<str> {
+    if s.is_empty() {
+        return Cow::Borrowed(s);
+    }
+
+    let mut leading_ws = usize::MAX;
+    let mut non_empty_lines = 0;
+
+    // Find the minimum number of possible leading units of whitespace that can be be stripped off
+    // of each non-empty line.
+    for line in s.lines().filter(|line| !line.is_empty()) {
+        let line_leading_ws = line.chars().take_while(|ch| ch.is_whitespace()).count();
+
+        if line_leading_ws == 0 {
+            // Fast path: no dedent needed if we encounter a non-empty line which starts with a
+            // non-whitespace character.
+            return Cow::Borrowed(s);
+        }
+
+        leading_ws = leading_ws.min(line_leading_ws);
+        non_empty_lines += 1;
+    }
+
+    // Strip the determined amount of leading whitespace off of each line.
+    let mut dedented = String::with_capacity(s.len() - leading_ws * non_empty_lines);
+
+    for line in s.lines() {
+        if !line.is_empty() {
+            dedented.extend(line.chars().skip(leading_ws));
+        }
+
+        dedented.push('\n');
+    }
+
+    if dedented.ends_with('\n') && !s.ends_with('\n') {
+        let new_len = dedented.len() - 1;
+        dedented.truncate(new_len);
+    }
+
+    Cow::Owned(dedented)
+}
+
+/// Takes in a string with backslash escapes written out with literal backslash characters and
+/// converts it to a string with the proper escaped characters.
+///
+/// ## Errors
+///
+/// Returns an error if an invalid or incomplete escape sequence or unicode code point is
+/// encountered.
+pub fn unescape(s: &str) -> Result<Cow<str>> {
+    for (idx, ch) in s.chars().enumerate() {
+        if ch == '\\' {
+            // At least one char needs unescaping so we need to return a new `String` instead of a
+            // borrowed `&str`.
+            return unescape_owned(s, idx).map(Cow::Owned);
+        }
+    }
+
+    Ok(Cow::Borrowed(s))
+}
+
+fn unescape_owned(s: &str, idx: usize) -> Result<String> {
+    let mut buf = String::with_capacity(s.len());
+
+    // Put all preceeding chars into buf already.
+    buf.push_str(&s[..idx]);
+
+    let mut chars = s[idx..].chars();
+    let mut scratch = String::new();
+
+    while let Some(ch) = chars.next() {
+        if ch != '\\' {
+            buf.push(ch);
+            continue;
+        }
+
+        let ch = match chars.next() {
+            Some('\n') => continue,
+            Some('b') => '\u{0008}',
+            Some('f') => '\u{000C}',
+            Some('n') => '\n',
+            Some('r') => '\r',
+            Some('t') => '\t',
+            Some('\'') => '\'',
+            Some('\"') => '\"',
+            Some('\\') => '\\',
+            Some('u') => match unescape_unicode(&mut chars, &mut scratch) {
+                Some(ch) => ch,
+                None => return Err(Error::InvalidUnicodeCodePoint(scratch)),
+            },
+            Some(ch) => return Err(Error::InvalidEscape(ch)),
+            None => return Err(Error::Eof),
+        };
+
+        buf.push(ch);
+    }
+
+    Ok(buf)
+}
+
+fn unescape_unicode(chars: &mut Chars<'_>, scratch: &mut String) -> Option<char> {
+    scratch.clear();
+
+    for _ in 0..4 {
+        scratch.push(chars.next()?);
+    }
+
+    char::from_u32(u32::from_str_radix(scratch, 16).ok()?)
+}
+
+/// Like [`unescape`], but returns the original `&str` if it contains invalid escape sequences
+/// instead of failing.
+pub fn try_unescape(s: &str) -> Cow<str> {
+    match unescape(s) {
+        Ok(s) => s,
+        Err(_) => Cow::Borrowed(s),
+    }
+}


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 5e7bbca..1e05794 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -263,10 +263,55 @@ fn parse_template() {
         rule: Rule::ExprTerm,
         tokens: [
             ExprTerm(0, 54, [
-                Heredoc(0, 54, [
-                    HeredocIntroNormal(0, 2),
-                    Identifier(2, 9),
-                    HeredocContent(10, 47)
+                TemplateExpr(0, 54, [
+                    HeredocTemplate(0, 54, [
+                        HeredocIntroNormal(0, 2),
+                        Identifier(2, 9),
+                        HeredocContent(10, 47)
+                    ])
+                ])
+            ])
+        ]
+    };
+
+    parses_to! {
+        parser: HclParser,
+        input: r#""foo ${bar} $${baz}, %{if cond ~} qux %{~ endif}""#,
+        rule: Rule::ExprTerm,
+        tokens: [
+            ExprTerm(0, 49, [
+                TemplateExpr(0, 49, [
+                    QuotedStringTemplate(0, 49, [
+                        QuotedStringTemplateInner(1, 48, [
+                            QuotedStringTemplateLiteral(1, 5),
+                            TemplateInterpolation(5, 11, [
+                                TemplateIExprStartNormal(5, 7),
+                                ExprTerm(7, 10, [
+                                    VariableExpr(7, 10)
+                                ]),
+                                TemplateExprEndNormal(10, 11)
+                            ]),
+                            QuotedStringTemplateLiteral(11, 21),
+                            TemplateDirective(21, 48, [
+                                TemplateIf(21, 48, [
+                                    TemplateIfExpr(21, 38, [
+                                        TemplateDExprStartNormal(21, 23),
+                                        ExprTerm(26, 31, [
+                                            VariableExpr(26, 30)
+                                        ]),
+                                        TemplateExprEndStrip(31, 33),
+                                        Template(34, 38, [
+                                            TemplateLiteral(34, 38)
+                                        ]),
+                                    ]),
+                                    TemplateEndIfExpr(38, 48, [
+                                        TemplateDExprStartStrip(38, 41),
+                                        TemplateExprEndNormal(47, 48),
+                                    ])
+                                ])
+                            ])
+                        ])
+                    ])
                 ])
             ])
         ]
@@ -283,28 +328,32 @@ fn parse_cond_in_interpolation() {
             Attribute(0, 37, [
                 Identifier(0, 4),
                 ExprTerm(7, 37, [
-                    StringLit(7, 37, [
-                        String(8, 36, [
-                            TemplateInterpolation(8, 36, [
-                                Conditional(10, 35, [
-                                    CondExpr(10, 15, [
-                                        ExprTerm(10, 15, [
-                                            VariableExpr(10, 13),
-                                            GetAttr(13, 15, [
-                                                Identifier(14, 15)
+                    TemplateExpr(7, 37, [
+                        QuotedStringTemplate(7, 37, [
+                            QuotedStringTemplateInner(8, 36, [
+                                TemplateInterpolation(8, 36, [
+                                    TemplateIExprStartNormal(8, 10),
+                                    Conditional(10, 35, [
+                                        CondExpr(10, 15, [
+                                            ExprTerm(10, 15, [
+                                                VariableExpr(10, 13),
+                                                GetAttr(13, 15, [
+                                                    Identifier(14, 15)
+                                                ])
+                                            ])
+                                        ]),
+                                        ExprTerm(18, 31, [
+                                            StringLit(18, 30, [
+                                                String(19, 29)
+                                            ])
+                                        ]),
+                                        ExprTerm(33, 35, [
+                                            StringLit(33, 35, [
+                                                String(34, 34)
                                             ])
                                         ])
                                     ]),
-                                    ExprTerm(18, 31, [
-                                        StringLit(18, 30, [
-                                            String(19, 29)
-                                        ])
-                                    ]),
-                                    ExprTerm(33, 35, [
-                                        StringLit(33, 35, [
-                                            String(34, 34)
-                                        ])
-                                    ])
+                                    TemplateExprEndNormal(35, 36)
                                 ])
                             ])
                         ])
@@ -543,8 +592,8 @@ fn unescape_strings() {
           }
 
           heredoc = <<-EOS
-            some string with \
-            escaped newline
+            heredoc template with \
+            escaped newline and \\backslash is not unescaped yet
           EOS
         }
     "#;
@@ -560,7 +609,16 @@ fn unescape_strings() {
                     "object_attr",
                     Expression::from_iter([("key\nwith\nnewlines", true)]),
                 ))
-                .add_attribute(("heredoc", "some string with escaped newline\n"))
+                .add_attribute((
+                    "heredoc",
+                    TemplateExpr::Heredoc(
+                        Heredoc::new(
+                            Identifier::new("EOS"),
+                            "            heredoc template with \\\n            escaped newline and \\\\backslash is not unescaped yet\n"
+                        )
+                        .with_strip_mode(HeredocStripMode::Indent)
+                    )
+                ))
                 .build(),
         )
         .build();
@@ -586,3 +644,47 @@ fn negative_numbers() {
 
     assert_eq!(body, expected);
 }
+
+#[test]
+fn template_expr() {
+    use crate::{
+        structure::TemplateExpr,
+        template::{IfDirective, IfExpr, StripMode, Template},
+    };
+
+    let input = r#"foo = "bar ${baz} %{~ if cond}qux%{ endif ~}""#;
+    let body = parse(input).unwrap();
+
+    let expected = Body::builder()
+        .add_attribute((
+            "foo",
+            TemplateExpr::QuotedString("bar ${baz} %{~ if cond}qux%{ endif ~}".into()),
+        ))
+        .build();
+
+    assert_eq!(body, expected);
+
+    match body.attributes().next().unwrap().expr() {
+        Expression::TemplateExpr(expr) => {
+            let template = Template::from_expr(expr).unwrap();
+
+            let expected_template = Template::new()
+                .add_literal("bar ")
+                .add_interpolation(Expression::Raw("baz".into()))
+                .add_literal(" ")
+                .add_directive(
+                    IfDirective::new(
+                        IfExpr::new(
+                            Expression::Raw("cond".into()),
+                            Template::new().add_literal("qux"),
+                        )
+                        .with_strip_mode(StripMode::Start),
+                    )
+                    .with_strip_mode(StripMode::End),
+                );
+
+            assert_eq!(template, expected_template);
+        }
+        _ => unreachable!(),
+    }
+}
diff --git a/src/ser/tests.rs b/src/ser/tests.rs
index 7fa4b32..e6f6f1a 100644
--- a/src/ser/tests.rs
+++ b/src/ser/tests.rs
@@ -1,5 +1,8 @@
 use super::*;
-use crate::{Block, BlockLabel, Body, Expression, Object, ObjectKey, RawExpression};
+use crate::{
+    Block, BlockLabel, Body, Expression, Heredoc, HeredocStripMode, Identifier, Object, ObjectKey,
+    RawExpression, TemplateExpr,
+};
 use pretty_assertions::assert_eq;
 use serde_json::json;
 
@@ -192,6 +195,46 @@ fn serialize_empty_block() {
     assert_eq!(to_string(&body).unwrap(), "empty {}\n");
 }
 
+#[test]
+fn serialize_heredoc() {
+    let body = Body::builder()
+        .add_block(
+            Block::builder("content")
+                .add_attribute((
+                    "heredoc",
+                    TemplateExpr::Heredoc(Heredoc::new(
+                        Identifier::new("HEREDOC"),
+                        "foo\n  bar\nbaz\n",
+                    )),
+                ))
+                .add_attribute((
+                    "heredoc_indent",
+                    TemplateExpr::Heredoc(
+                        Heredoc::new(Identifier::new("HEREDOC"), "    foo\n      bar\n    baz\n")
+                            .with_strip_mode(HeredocStripMode::Indent),
+                    ),
+                ))
+                .build(),
+        )
+        .build();
+
+    let expected = r#"content {
+  heredoc = <<HEREDOC
+foo
+  bar
+baz
+HEREDOC
+  heredoc_indent = <<-HEREDOC
+    foo
+      bar
+    baz
+  HEREDOC
+}
+"#;
+
+    assert_eq!(to_string(&body).unwrap(), expected);
+}
+
 #[test]
 fn serialize_errors() {
     assert!(to_string(&true).is_err());
@@ -280,17 +323,15 @@ fn roundtrip() {
                 .add_attribute((
                     "tags",
                     Expression::from_iter([
-                        (
-                            ObjectKey::String("${var.dynamic}".into()),
-                            Expression::Bool(true),
-                        ),
                         (
                             ObjectKey::String("application".into()),
                             Expression::String("myapp".into()),
                         ),
                         (
                             ObjectKey::Identifier("team".into()),
-                            Expression::String("bar".into()),
+                            Expression::TemplateExpr(Box::new(TemplateExpr::QuotedString(
+                                "${var.team}".into(),
+                            ))),
                         ),
                     ]),
                 ))
diff --git a/src/structure/ser/tests.rs b/src/structure/ser/tests.rs
index 344ac74..06fe69c 100644
--- a/src/structure/ser/tests.rs
+++ b/src/structure/ser/tests.rs
@@ -3,9 +3,13 @@ use super::{
     block::{BlockLabelSerializer, BlockSerializer},
     body::BodySerializer,
     expression::ExpressionSerializer,
+    template::TemplateExprSerializer,
     *,
 };
-use crate::{Attribute, Block, BlockLabel, Body, Expression, Map};
+use crate::{
+    structure::Identifier, Attribute, Block, BlockLabel, Body, Expression, Heredoc,
+    HeredocStripMode, Map, TemplateExpr,
+};
 use serde::Serialize;
 use std::fmt::Debug;
 
@@ -57,6 +61,21 @@ fn identity() {
         ExpressionSerializer,
         Expression::from_iter([("foo", "bar")]),
     );
+    test_identity(
+        TemplateExprSerializer,
+        TemplateExpr::QuotedString("${foo}".into()),
+    );
+    test_identity(
+        TemplateExprSerializer,
+        TemplateExpr::Heredoc(
+            Heredoc::new(Identifier::new("EOS"), "  ${foo}")
+                .with_strip_mode(HeredocStripMode::Indent),
+        ),
+    );
+    test_identity(
+        TemplateExprSerializer,
+        TemplateExpr::Heredoc(Heredoc::new(Identifier::new("EOS"), "${foo}")),
+    );
 }
 
 #[test]
diff --git a/src/structure/tests.rs b/src/structure/tests.rs
index a0076ed..884c455 100644
--- a/src/structure/tests.rs
+++ b/src/structure/tests.rs
@@ -27,6 +27,16 @@ fn body_into_value() {
                 .build(),
         )
         .add_attribute(("foo", "baz"))
+        .add_attribute((
+            "heredoc",
+            TemplateExpr::Heredoc(
+                Heredoc::new(
+                    Identifier::new("EOS"),
+                    "  foo \\\n  bar ${baz}\\\\backslash",
+                )
+                .with_strip_mode(HeredocStripMode::Indent),
+            ),
+        ))
         .build();
 
     let value = json!({
@@ -43,8 +53,9 @@ fn body_into_value() {
             ],
             "qux": {
                 "foo": 1
-            }
-        }
+            },
+        },
+        "heredoc": "foo bar ${baz}\\backslash"
     });
 
     let expected: Value = serde_json::from_value(value).unwrap();