Repository: LukasKalbertodt/bunt (language: rust)

Issue:
Add support for width and precision parameters
The `std::fmt` syntax allows to pass the width or precision via parameter. It looks like this:

```rust
println!("Hello {1:0$}!", 5, "x");
```

This is currently unsupported and leads to strange behavior or errors.


Discussion / hints:

Fix patch:
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
index de82405..b3f0c0a 100644
--- a/macros/Cargo.toml
+++ b/macros/Cargo.toml
@@ -11,5 +11,6 @@ license = "MIT/Apache-2.0"
 proc-macro = true
 
 [dependencies]
-quote = "1"
 proc-macro2 = "1.0.19"
+quote = "1"
+unicode-xid = "0.2.1"
diff --git a/macros/src/err.rs b/macros/src/err.rs
index d56d1f7..d1793f4 100644
--- a/macros/src/err.rs
+++ b/macros/src/err.rs
@@ -20,7 +20,9 @@ impl Error {
     pub(crate) fn to_compile_error(&self) -> TokenStream {
         let msg = &self.msg;
         quote_spanned! {self.span=>
-            compile_error!(#msg);
+            {
+                compile_error!(#msg);
+            }
         }
     }
 }
diff --git a/macros/src/gen.rs b/macros/src/gen.rs
index 8b48706..f69eaf1 100644
--- a/macros/src/gen.rs
+++ b/macros/src/gen.rs
@@ -3,23 +3,20 @@
 use proc_macro2::{Ident, Span, TokenStream};
 use quote::quote;
 use std::{
+    cell::RefCell,
     collections::BTreeSet,
-    fmt::Write,
+    fmt::{self, Write},
 };
 use crate::{
     err::Error,
-    ir::{WriteInput, FormatStrFragment, ArgRefKind, Style, Color, Expr},
+    ir::{WriteInput, FormatStrFragment, ArgRefKind, Style, Color, Expr, FormatSpec, Align, Sign, Width, Precision},
 };
 
 
 impl WriteInput {
     pub(crate) fn gen_output(&self) -> Result<TokenStream, Error> {
-        // Helper functions to create idents for argument bindings
-        fn pos_arg_ident(id: u32) -> Ident {
-            Ident::new(&format!("arg_pos_{}", id), Span::mixed_site())
-        }
-        fn name_arg_ident(id: &str) -> Ident {
-            Ident::new(&format!("arg_name_{}", id), Span::mixed_site())
+        fn arg_ident(id: usize) -> Ident {
+            Ident::new(&format!("arg{}", id), Span::mixed_site())
         }
 
         // Create a binding for each given argument. This is useful for two
@@ -32,14 +29,8 @@ impl WriteInput {
         //   bindings, we have to do lots of tricky logic to get the right
         //   arguments in each invidiual `write` call.
         let mut arg_bindings = TokenStream::new();
-        for (i, arg) in self.args.positional.iter().enumerate() {
-            let ident = pos_arg_ident(i as u32);
-            arg_bindings.extend(quote! {
-                let #ident = &#arg;
-            })
-        }
-        for (name, arg) in self.args.named.iter() {
-            let ident = name_arg_ident(name);
+        for (i, arg) in self.args.exprs.iter().enumerate() {
+            let ident = arg_ident(i);
             arg_bindings.extend(quote! {
                 let #ident = &#arg;
             })
@@ -52,6 +43,14 @@ impl WriteInput {
         let mut writes = TokenStream::new();
         let mut next_arg_index = 0;
 
+        // We want to keep track of all expressions that were used somehow to
+        // emit an error if there are unused ones. As the easiest way to update
+        // this is within `ident_for_pos` and `ident_for_name` and since two
+        // closures cannot borrow the vector mutably at the same time, we use a
+        // ref cell. It's totally fine here as the vector will only be borrowed
+        // very briefly.
+        let used_expressions = RefCell::new(vec![false; self.args.exprs.len()]);
+
         for segment in &self.format_str.fragments {
             match segment {
                 // A formatting fragment. This is the more tricky one. We have
@@ -59,46 +58,111 @@ impl WriteInput {
                 // fmt string, the right arguments (and no additional ones!) and
                 // the correct argument references.
                 FormatStrFragment::Fmt { fmt_str_parts, args } => {
+                    // Two helper functions to get the correct identifier for an
+                    // argument.
+                    let ident_for_pos = |pos| -> Result<Ident, Error> {
+                        if self.args.exprs.get(pos).is_none() {
+                            return Err(err!(
+                                "invalid reference to positional argument {} (there are \
+                                    not that many arguments)",
+                                pos,
+                            ));
+                        }
+
+                        used_expressions.borrow_mut()[pos] = true;
+                        Ok(arg_ident(pos))
+                    };
+                    let ident_for_name = |name| -> Result<Ident, Error> {
+                        let index = self.args.name_indices.get(name)
+                            .ok_or(err!("there is no argument named `{}`", name))?;
+
+                        used_expressions.borrow_mut()[*index] = true;
+                        Ok(arg_ident(*index))
+                    };
+
                     let mut fmt_str = fmt_str_parts[0].clone();
                     let mut used_args = BTreeSet::new();
 
                     for (i, arg) in args.into_iter().enumerate() {
-                        let ident = match &arg.kind {
-                            ArgRefKind::Next => {
-                                let ident = pos_arg_ident(next_arg_index as u32);
-                                if self.args.positional.get(next_arg_index).is_none() {
-                                    return Err(
-                                        err!("invalid '{{}}' argument reference \
-                                            (too few actual arguments)")
-                                    );
-                                }
+                        // Check width and precision parameters. Those are the
+                        // only two things in the format spec that can refer to
+                        // arguments. If they do, we change them to always refer
+                        // to a named parameter hat is inserted into
+                        // `used_args`.
+                        //
+                        // We check those before the main argument because of
+                        // the `.*` precision modifier which, like `{}`, refers
+                        // to the next argument. BUT the `.*` comes first. So
+                        // `println!("{:.*}", 2, 3.1415926)` prints `3.14` and
+                        // swapping the arguments would result in an error.
+                        let mut format_spec = arg.format_spec.clone();
 
-                                next_arg_index += 1;
-                                ident
+                        match &arg.format_spec.width {
+                            None | Some(Width::Constant(_)) => {}
+                            Some(Width::Name(name)) => {
+                                let ident = ident_for_name(name)?;
+                                format_spec.width = Some(Width::Name(ident.to_string()));
+                                used_args.insert(ident);
+                            }
+                            Some(Width::Position(pos)) => {
+                                let ident = ident_for_pos(*pos)?;
+                                format_spec.width = Some(Width::Name(ident.to_string()));
+                                used_args.insert(ident);
                             }
-                            ArgRefKind::Position(pos) => {
-                                let ident = pos_arg_ident(*pos);
-                                if self.args.positional.get(*pos as usize).is_none() {
+                        }
+
+                        match &arg.format_spec.precision {
+                            None | Some(Precision::Constant(_)) => {}
+                            Some(Precision::Name(name)) => {
+                                let ident = ident_for_name(name)?;
+                                format_spec.precision = Some(Precision::Name(ident.to_string()));
+                                used_args.insert(ident);
+                            }
+                            Some(Precision::Position(pos)) => {
+                                let ident = ident_for_pos(*pos)?;
+                                format_spec.precision = Some(Precision::Name(ident.to_string()));
+                                used_args.insert(ident);
+                            }
+                            Some(Precision::Bundled) => {
+                                if self.args.exprs.get(next_arg_index).is_none() {
                                     return Err(err!(
-                                        "invalid reference to positional argument {} (there are \
-                                            not that many arguments)",
-                                        pos,
+                                        "invalid '.*' precision argument reference to \
+                                            argument {} (too few actual arguments)",
+                                        next_arg_index,
                                     ));
                                 }
 
-                                ident
+                                let ident = arg_ident(next_arg_index);
+                                format_spec.precision = Some(Precision::Name(ident.to_string()));
+
+                                used_expressions.borrow_mut()[next_arg_index] = true;
+                                used_args.insert(ident);
+                                next_arg_index += 1;
                             }
-                            ArgRefKind::Name(name) => {
-                                let ident = name_arg_ident(&name);
-                                if self.args.named.get(name).is_none() {
-                                    return Err(err!("there is no argument named `{}`", name));
+                        }
+
+                        // Check the main argument.
+                        let ident = match &arg.kind {
+                            ArgRefKind::Next => {
+                                if self.args.exprs.get(next_arg_index).is_none() {
+                                    return Err(
+                                        err!("invalid '{{}}' argument reference \
+                                            (too few actual arguments)")
+                                    );
                                 }
 
+                                used_expressions.borrow_mut()[next_arg_index] = true;
+                                let ident = arg_ident(next_arg_index);
+                                next_arg_index += 1;
                                 ident
                             }
+                            ArgRefKind::Position(pos) => ident_for_pos(*pos)?,
+                            ArgRefKind::Name(name) => ident_for_name(name)?,
                         };
 
-                        std::write!(fmt_str, "{{{}{}}}", ident, arg.format_spec).unwrap();
+                        // Create the full fmt argument and also push the next
+                        // string part.
+                        std::write!(fmt_str, "{{{}:{}}}", ident, format_spec).unwrap();
                         used_args.insert(ident);
                         fmt_str.push_str(&fmt_str_parts[i + 1]);
                     }
@@ -142,6 +206,11 @@ impl WriteInput {
             return Err(err!("unclosed style tag"));
         }
 
+        if let Some(unused_pos) = used_expressions.into_inner().iter().position(|used| !used) {
+            let expr = &self.args.exprs[unused_pos];
+            return Err(err!(expr.span, "argument never used: `{}`", expr.tokens));
+        }
+
         // Combine everything.
         let target = &self.target;
         Ok(quote! {
@@ -164,6 +233,56 @@ impl quote::ToTokens for Expr {
     }
 }
 
+impl fmt::Display for FormatSpec {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Some(fill) = self.fill {
+            f.write_char(fill)?;
+        }
+        if let Some(align) = self.align {
+            let c = match align {
+                Align::Left => '<',
+                Align::Center => '^',
+                Align::Right => '>',
+            };
+            f.write_char(c)?;
+        }
+        if let Some(sign) = self.sign {
+            let c = match sign {
+                Sign::Plus => '+',
+                Sign::Minus => '-',
+            };
+            f.write_char(c)?;
+        }
+        if self.alternate {
+            f.write_char('#')?;
+        }
+        if self.zero {
+            f.write_char('0')?;
+        }
+
+        match &self.width {
+            Some(Width::Constant(n)) => write!(f, "{}", n)?,
+            Some(Width::Position(n)) => write!(f, "{}$", n)?,
+            Some(Width::Name(s)) => write!(f, "{}$", s)?,
+            None => {}
+        }
+
+        match &self.precision {
+            Some(Precision::Constant(n)) => write!(f, ".{}", n)?,
+            Some(Precision::Position(n)) => write!(f, ".{}$", n)?,
+            Some(Precision::Name(s)) => write!(f, ".{}$", s)?,
+            Some(Precision::Bundled) => write!(f, ".*")?,
+            None => {}
+        }
+
+        if let Some(t) = self.ty {
+            f.write_char(t)?;
+        }
+
+        Ok(())
+    }
+}
+
 impl Style {
     /// Returns a token stream representing an expression constructing the
     /// `ColorSpec` value corresponding to `self`.
diff --git a/macros/src/ir.rs b/macros/src/ir.rs
index 3ed0aa2..05f344c 100644
--- a/macros/src/ir.rs
+++ b/macros/src/ir.rs
@@ -76,7 +76,7 @@ pub(crate) enum FormatStrFragment {
 #[derive(Debug)]
 pub(crate) struct ArgRef {
     pub(crate) kind: ArgRefKind,
-    pub(crate) format_spec: String,
+    pub(crate) format_spec: FormatSpec,
 }
 
 /// How a format argument is referred to.
@@ -85,16 +85,82 @@ pub(crate) enum ArgRefKind {
     /// `{}`
     Next,
     /// `{2}`
-    Position(u32),
+    Position(usize),
     /// `{peter}`
     Name(String),
 }
 
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
+pub(crate) struct FormatSpec {
+    pub(crate) fill: Option<char>,
+    pub(crate) align: Option<Align>,
+    pub(crate) sign: Option<Sign>,
+    pub(crate) alternate: bool,
+    pub(crate) zero: bool,
+    pub(crate) width: Option<Width>,
+    pub(crate) precision: Option<Precision>,
+    pub(crate) ty: Option<char>,
+}
+
+#[cfg(test)]
+impl Default for FormatSpec {
+    fn default() -> Self {
+        Self {
+            fill: None,
+            align: None,
+            sign: None,
+            alternate: false,
+            zero: false,
+            width: None,
+            precision: None,
+            ty: None,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(test, derive(PartialEq))]
+pub(crate) enum Align {
+    Left,
+    Center,
+    Right,
+}
+
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(test, derive(PartialEq))]
+pub(crate) enum Sign {
+    Plus,
+    Minus,
+}
+
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
+pub(crate) enum Width {
+    Constant(usize),
+    Name(String),
+    Position(usize),
+}
+
+#[derive(Debug, Clone)]
+#[cfg_attr(test, derive(PartialEq))]
+pub(crate) enum Precision {
+    Constant(usize),
+    Name(String),
+    Position(usize),
+    /// `.*`
+    Bundled,
+}
+
 /// Parsed formatting arguments.
 #[derive(Debug)]
 pub(crate) struct FormatArgs {
-    pub(crate) positional: Vec<Expr>,
-    pub(crate) named: HashMap<String, Expr>,
+    /// All argument expressions in order, including the named ones (without the
+    /// `name =` part).
+    pub(crate) exprs: Vec<Expr>,
+
+    /// Mapping from named argument name to index in `self.exprs`.
+    pub(crate) name_indices: HashMap<String, usize>,
 }
 
 #[derive(Debug, Clone, Copy)]
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 7b0d442..0be2ba9 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -8,9 +8,11 @@ use proc_macro2::TokenStream;
 mod err;
 mod gen;
 mod ir;
-mod literal;
 mod parse;
 
+#[cfg(test)]
+mod tests;
+
 use crate::{
     err::Error,
     ir::{Style, WriteInput},
diff --git a/macros/src/parse.rs b/macros/src/parse.rs
deleted file mode 100644
index f260158..0000000
--- a/macros/src/parse.rs
+++ /dev/null
@@ -1,427 +0,0 @@
-//! Parse the macro input into an intermediate representation.
-
-use proc_macro2::{
-    Span, TokenStream, Delimiter, TokenTree, Spacing,
-    token_stream::IntoIter as TokenIterator,
-};
-use std::collections::HashMap;
-use crate::{
-    err::Error,
-    ir::{
-        ArgRefKind, ArgRef, Expr, WriteInput, FormatStr, Style, Color,
-        FormatStrFragment, FormatArgs,
-    },
-};
-
-
-/// Helper function to parse from a token stream. Makes sure the iterator is
-/// empty after `f` returns.
-pub(crate) fn parse<F, O>(tokens: TokenStream, f: F) -> Result<O, Error>
-where
-    F: FnOnce(&mut TokenIterator) -> Result<O, Error>,
-{
-    let mut it = tokens.into_iter();
-    let out = f(&mut it)?;
-
-    if let Some(tt) = it.next() {
-        return Err(err!(tt.span(), "unexpected additional tokens"));
-    }
-
-    Ok(out)
-}
-
-/// Tries to parse a string literal.
-fn parse_str_literal(it: &mut TokenIterator) -> Result<(String, Span), Error> {
-    match it.next() {
-        Some(TokenTree::Literal(lit)) => Ok((
-            crate::literal::parse_str_literal(&lit)?,
-            lit.span(),
-        )),
-        Some(tt) => {
-            Err(err!(tt.span(), "expected string literal, found different token tree"))
-        }
-        None => Err(err!("expected string literal, found EOF")),
-    }
-}
-
-/// Tries to parse a helper group (a group with `None` or `()` delimiter).
-///
-/// These groups are inserted by the declarative macro wrappers in `bunt` to
-/// make parsing in `bunt-macros` easier. In particular, all expressions are
-/// wrapped in these group, allowing us to skip over them without having a Rust
-/// expression parser.
-fn expect_helper_group(tt: Option<TokenTree>) -> Result<(TokenStream, Span), Error> {
-    match tt {
-        Some(TokenTree::Group(g))
-            if g.delimiter() == Delimiter::None || g.delimiter() == Delimiter::Parenthesis =>
-        {
-            Ok((g.stream(), g.span()))
-        }
-        Some(TokenTree::Group(g)) => {
-            Err(err!(
-                g.span(),
-                "expected none or () delimited group, but delimiter is {:?} (note: do not use \
-                    the macros from `bunt-macros` directly, but only through `bunt`)",
-                g.delimiter(),
-            ))
-        }
-        Some(tt) => {
-            Err(err!(
-                tt.span(),
-                "expected none or () delimited group, but found different token tree (note: do \
-                    not use the macros from `bunt-macros` directly, but only through `bunt`)",
-            ))
-        }
-        None => Err(err!("expected none or () delimited group, found EOF")),
-    }
-}
-
-impl WriteInput {
-    pub(crate) fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
-        let target = Expr::parse(it)?;
-        let format_str = FormatStr::parse(it)?;
-        let args = FormatArgs::parse(it)?;
-
-        Ok(Self { target, format_str, args })
-    }
-}
-
-impl Expr {
-    pub(crate) fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
-        let (tokens, span) = expect_helper_group(it.next())?;
-        Ok(Self { tokens, span })
-    }
-}
-
-impl FormatStr {
-    pub(crate) fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
-        /// Searches for the next closing `}`. Returns a pair of strings, the
-        /// first starting like `s` and ending at the closing brace, the second
-        /// starting at the brace and ending like `s`. Both strings exclude the
-        /// brace itself. If a closing brace can't be found, an error is
-        /// returned.
-        fn split_at_closing_brace(s: &str, span: Span) -> Result<(&str, &str), Error> {
-            // I *think* there can't be escaped closing braces inside the fmt
-            // format, so we can simply search for a single closing one.
-            let end = s.find("}")
-                .ok_or(err!(span, "unclosed '{{' in format string"))?;
-            Ok((&s[..end], &s[end + 1..]))
-        }
-
-        let (inner, _) = expect_helper_group(it.next())?;
-        let (raw, span) = parse(inner, parse_str_literal)?;
-
-        // Scan the whole string
-        let mut fragments = Vec::new();
-        let mut s = &raw[..];
-        while !s.is_empty() {
-            fn string_without<'a>(a: &'a str, b: &'a str) -> &'a str {
-                let end = b.as_ptr() as usize - a.as_ptr() as usize;
-                &a[..end]
-            }
-
-            // let start_string = s;
-            let mut args = Vec::new();
-            let mut fmt_str_parts = Vec::new();
-
-            // Scan until we reach a style tag.
-            let mut scanner = s;
-            loop {
-                match scanner.find('{') {
-                    Some(brace_pos) => scanner = &scanner[brace_pos..],
-                    None => {
-                        // EOF reached: stop searching
-                        scanner = &scanner[scanner.len()..];
-                        break;
-                    }
-                }
-
-
-                match () {
-                    // Escaped brace: skip.
-                    () if scanner.starts_with("{{") => scanner = &scanner[2..],
-
-                    // Found a style tag: stop searching!
-                    () if scanner.starts_with("{$") => break,
-                    () if scanner.starts_with("{/$") => break,
-
-                    // Found a styled argument: stop searching!
-                    () if scanner.starts_with("{[") => break,
-
-                    // An formatting argument. Gather some information about it
-                    // and remember it for later.
-                    _ => {
-                        let (inner, rest) = split_at_closing_brace(&scanner[1..], span)?;
-                        args.push(ArgRef::parse(inner)?);
-                        fmt_str_parts.push(string_without(s, scanner).to_owned());
-                        s = rest;
-                        scanner = rest;
-                    }
-                }
-            }
-
-            // Add the last string part and then push this fragment, unless it
-            // is completely empty.
-            fmt_str_parts.push(string_without(s, scanner).to_owned());
-            s = scanner;
-            if !args.is_empty() || fmt_str_parts.iter().any(|s| !s.is_empty()) {
-                fragments.push(FormatStrFragment::Fmt { args, fmt_str_parts });
-            }
-
-            if s.is_empty() {
-                break;
-            }
-
-            // At this point, `s` starts with either a styled argument or a
-            // style tag.
-            match () {
-                // Closing style tag.
-                () if s.starts_with("{/$}") => {
-                    fragments.push(FormatStrFragment::StyleEnd);
-                    s = &s[4..];
-                }
-
-                // Opening style tag.
-                () if s.starts_with("{$") => {
-                    let (inner, rest) = split_at_closing_brace(&s[2..], span)?;
-                    let style = Style::parse(inner, span)?;
-                    fragments.push(FormatStrFragment::StyleStart(style));
-                    s = rest;
-                }
-
-                () if s.starts_with("{[") => {
-                    let (inner, rest) = split_at_closing_brace(&s[1..], span)?;
-
-                    // Parse style information
-                    let style_end = inner.find(']')
-                        .ok_or(err!(span, "unclosed '[' in format string argument"))?;
-                    let style = Style::parse(&inner[1..style_end], span)?;
-                    fragments.push(FormatStrFragment::StyleStart(style));
-
-                    // Parse the standard part of this arg reference.
-                    let standard_inner = inner[style_end + 1..].trim_start();
-                    let arg = ArgRef::parse(standard_inner)?;
-                    fragments.push(FormatStrFragment::Fmt {
-                        args: vec![arg],
-                        fmt_str_parts: vec!["".into(), "".into()],
-                    });
-
-                    fragments.push(FormatStrFragment::StyleEnd);
-
-                    s = rest;
-                }
-
-                _ => panic!("bug: at this point, there should be a style tag or styled arg"),
-            }
-        }
-
-        Ok(Self { fragments })
-    }
-}
-
-impl ArgRef {
-    /// (Partially) parses the inside of an format arg (`{...}`). The given
-    /// string `s` must be the inside of the arg and must *not* contain the
-    /// outer braces.
-    pub(crate) fn parse(s: &str) -> Result<Self, Error> {
-        // Split argument reference and format specs.
-        let arg_ref_end = s.find(':').unwrap_or(s.len());
-        let (arg_str, format_spec) = s.split_at(arg_ref_end);
-
-        // Check kind of argument reference.
-        let kind = if arg_str.is_empty() {
-            ArgRefKind::Next
-        } else if let Ok(pos) = arg_str.parse::<u32>() {
-            ArgRefKind::Position(pos)
-        } else {
-            // TODO: make sure the string is a valid Rust identifier
-            ArgRefKind::Name(arg_str.into())
-        };
-
-        Ok(Self { kind, format_spec: format_spec.into() })
-    }
-}
-
-impl Style {
-    /// Parses the style specifiction assuming the token stream contains a
-    /// single string literal.
-    pub(crate) fn parse_from_tokens(tokens: TokenStream) -> Result<Self, Error> {
-        let (s, span) = parse(tokens, parse_str_literal)?;
-        Self::parse(&s, span)
-    }
-
-    /// Parses the style specification in `spec` (with `span`) and returns a token
-    /// stream representing an expression constructing the corresponding `ColorSpec`
-    /// value.
-    fn parse(spec: &str, span: Span) -> Result<Self, Error> {
-        let mut out = Self::default();
-
-        let mut previous_fg_color = None;
-        let mut previous_bg_color = None;
-        for fragment in spec.split('+').map(str::trim).filter(|s| !s.is_empty()) {
-            let (fragment, is_bg) = match fragment.strip_prefix("bg:") {
-                Some(color) => (color, true),
-                None => (fragment, false),
-            };
-
-            // Parse/obtain color if a color is specified.
-            let color = match fragment {
-                "black" => Some(Color::Black),
-                "blue" => Some(Color::Blue),
-                "green" => Some(Color::Green),
-                "red" => Some(Color::Red),
-                "cyan" => Some(Color::Cyan),
-                "magenta" => Some(Color::Magenta),
-                "yellow" => Some(Color::Yellow),
-                "white" => Some(Color::White),
-
-                hex if hex.starts_with('#') => {
-                    let hex = &hex[1..];
-
-                    if hex.len() != 6 {
-                        let e = err!(
-                            span,
-                            "hex color code invalid: 6 digits expected, found {}",
-                            hex.len(),
-                        );
-                        return Err(e);
-                    }
-
-                    let digits = hex.chars()
-                        .map(|c| {
-                            c.to_digit(16).ok_or_else(|| {
-                                err!(span, "hex color code invalid: {} is not a valid hex digit", c)
-                            })
-                        })
-                        .collect::<Result<Vec<_>, _>>()?;
-
-                    let r = (digits[0] * 16 + digits[1]) as u8;
-                    let g = (digits[2] * 16 + digits[3]) as u8;
-                    let b = (digits[4] * 16 + digits[5]) as u8;
-
-                    Some(Color::Rgb(r, g, b))
-                },
-
-                // TODO: Ansi256 colors
-                _ => None,
-            };
-
-            // Check for duplicate color definitions.
-            let (previous_color, color_kind) = match is_bg {
-                true => (&mut previous_bg_color, "background"),
-                false => (&mut previous_fg_color, "foreground"),
-            };
-            match (&color, *previous_color) {
-                (Some(_), Some(old)) => {
-                    let e = err!(
-                        span,
-                        "found '{}' but the {} color was already specified as '{}'",
-                        fragment,
-                        color_kind,
-                        old,
-                    );
-                    return Err(e);
-                }
-                (Some(_), None) => *previous_color = Some(fragment),
-                _ => {}
-            }
-
-            macro_rules! set_attr {
-                ($field:ident, $value:expr) => {{
-                    if let Some(b) = out.$field {
-                        let field_s = stringify!($field);
-                        let old = if b { field_s.into() } else { format!("!{}", field_s) };
-                        let new = if $value { field_s.into() } else { format!("!{}", field_s) };
-                        let e = err!(
-                            span,
-                            "invalid style definition: found '{}', but '{}' was specified before",
-                            new,
-                            old,
-                        );
-                        return Err(e);
-                    }
-
-                    out.$field = Some($value);
-                }};
-            }
-
-            // Obtain the final token stream for method call.
-            match (is_bg, color, fragment) {
-                (false, Some(color), _) => out.fg = Some(color),
-                (true, Some(color), _) => out.bg = Some(color),
-                (true, None, other) => {
-                    return Err(err!(span, "'{}' (following 'bg:') is not a valid color", other));
-                }
-
-                (false, None, "bold") => set_attr!(bold, true),
-                (false, None, "!bold") => set_attr!(bold, false),
-                (false, None, "italic") => set_attr!(italic, true),
-                (false, None, "!italic") => set_attr!(italic, false),
-                (false, None, "underline") => set_attr!(underline, true),
-                (false, None, "!underline") => set_attr!(underline, false),
-                (false, None, "intense") => set_attr!(intense, true),
-                (false, None, "!intense") => set_attr!(intense, false),
-
-                (false, None, other) => {
-                    return Err(err!(span, "invalid style spec fragment '{}'", other));
-                }
-            }
-        }
-
-        Ok(out)
-    }
-}
-
-impl FormatArgs {
-    fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
-        /// Checks if the token stream starting with `tt0` and `tt1` is a named
-        /// argument. If so, returns the name of the argument, otherwise
-        /// (positional argument) returns `None`.
-        fn get_name(tt0: &Option<TokenTree>, tt1: &Option<TokenTree>) -> Option<String> {
-            if let (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(punct))) = (tt0, tt1) {
-                if punct.as_char() == '=' && punct.spacing() == Spacing::Alone {
-                    return Some(name.to_string())
-                }
-            }
-
-            None
-        }
-
-        let mut positional = Vec::new();
-        let mut named = HashMap::new();
-        let mut saw_named = false;
-
-        // The remaining tokens should all be `None` delimited groups each
-        // representing one argument.
-        for arg_group in it {
-            let (arg, span) = expect_helper_group(Some(arg_group))?;
-            let mut it = arg.into_iter();
-            let tt0 = it.next();
-            let tt1 = it.next();
-
-            if let Some(name) = get_name(&tt0, &tt1) {
-                saw_named = true;
-
-                let expr = Expr {
-                    tokens: it.collect(),
-                    span,
-                };
-                named.insert(name, expr);
-            } else {
-                if saw_named {
-                    let e = err!(span, "positional argument after named arguments is not allowed");
-                    return Err(e);
-                }
-
-                let expr = Expr {
-                    tokens: vec![tt0, tt1].into_iter().filter_map(|tt| tt).chain(it).collect(),
-                    span,
-                };
-                positional.push(expr);
-            }
-
-        }
-
-        Ok(Self { positional, named })
-    }
-}
diff --git a/macros/src/parse/fmt.rs b/macros/src/parse/fmt.rs
new file mode 100644
index 0000000..c04ff4a
--- /dev/null
+++ b/macros/src/parse/fmt.rs
@@ -0,0 +1,379 @@
+use proc_macro2::{
+    Span,
+    token_stream::IntoIter as TokenIterator,
+};
+use unicode_xid::UnicodeXID;
+use std::str::Chars;
+use crate::{
+    err::Error,
+    ir::{
+        ArgRefKind, ArgRef, FormatStr, Style,
+        FormatStrFragment, FormatSpec, Align, Sign, Width, Precision,
+    },
+};
+use super::{parse, expect_helper_group, lit::expect_str_literal};
+
+
+impl FormatStr {
+    pub(crate) fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
+        /// Searches for the next closing `}`. Returns a pair of strings, the
+        /// first starting like `s` and ending at the closing brace, the second
+        /// starting at the brace and ending like `s`. Both strings exclude the
+        /// brace itself. If a closing brace can't be found, an error is
+        /// returned.
+        fn split_at_closing_brace(s: &str, span: Span) -> Result<(&str, &str), Error> {
+            // I *think* there can't be escaped closing braces inside the fmt
+            // format, so we can simply search for a single closing one.
+            let end = s.find("}")
+                .ok_or(err!(span, "unclosed '{{' in format string"))?;
+            Ok((&s[..end], &s[end + 1..]))
+        }
+
+        let (inner, _) = expect_helper_group(it.next())?;
+        let (raw, span) = parse(inner, expect_str_literal)?;
+
+        // Scan the whole string
+        let mut fragments = Vec::new();
+        let mut s = &raw[..];
+        while !s.is_empty() {
+            fn string_without<'a>(a: &'a str, b: &'a str) -> &'a str {
+                let end = b.as_ptr() as usize - a.as_ptr() as usize;
+                &a[..end]
+            }
+
+            // let start_string = s;
+            let mut args = Vec::new();
+            let mut fmt_str_parts = Vec::new();
+
+            // Scan until we reach a style tag.
+            let mut scanner = s;
+            loop {
+                match scanner.find('{') {
+                    Some(brace_pos) => scanner = &scanner[brace_pos..],
+                    None => {
+                        // EOF reached: stop searching
+                        scanner = &scanner[scanner.len()..];
+                        break;
+                    }
+                }
+
+
+                match () {
+                    // Escaped brace: skip.
+                    () if scanner.starts_with("{{") => scanner = &scanner[2..],
+
+                    // Found a style tag: stop searching!
+                    () if scanner.starts_with("{$") => break,
+                    () if scanner.starts_with("{/$") => break,
+
+                    // Found a styled argument: stop searching!
+                    () if scanner.starts_with("{[") => break,
+
+                    // An formatting argument. Gather some information about it
+                    // and remember it for later.
+                    _ => {
+                        let (inner, rest) = split_at_closing_brace(&scanner[1..], span)?;
+                        args.push(ArgRef::parse(inner)?);
+                        fmt_str_parts.push(string_without(s, scanner).to_owned());
+                        s = rest;
+                        scanner = rest;
+                    }
+                }
+            }
+
+            // Add the last string part and then push this fragment, unless it
+            // is completely empty.
+            fmt_str_parts.push(string_without(s, scanner).to_owned());
+            s = scanner;
+            if !args.is_empty() || fmt_str_parts.iter().any(|s| !s.is_empty()) {
+                fragments.push(FormatStrFragment::Fmt { args, fmt_str_parts });
+            }
+
+            if s.is_empty() {
+                break;
+            }
+
+            // At this point, `s` starts with either a styled argument or a
+            // style tag.
+            match () {
+                // Closing style tag.
+                () if s.starts_with("{/$}") => {
+                    fragments.push(FormatStrFragment::StyleEnd);
+                    s = &s[4..];
+                }
+
+                // Opening style tag.
+                () if s.starts_with("{$") => {
+                    let (inner, rest) = split_at_closing_brace(&s[2..], span)?;
+                    let style = Style::parse(inner, span)?;
+                    fragments.push(FormatStrFragment::StyleStart(style));
+                    s = rest;
+                }
+
+                () if s.starts_with("{[") => {
+                    let (inner, rest) = split_at_closing_brace(&s[1..], span)?;
+
+                    // Parse style information
+                    let style_end = inner.find(']')
+                        .ok_or(err!(span, "unclosed '[' in format string argument"))?;
+                    let style = Style::parse(&inner[1..style_end], span)?;
+                    fragments.push(FormatStrFragment::StyleStart(style));
+
+                    // Parse the standard part of this arg reference.
+                    let standard_inner = inner[style_end + 1..].trim_start();
+                    let arg = ArgRef::parse(standard_inner)?;
+                    fragments.push(FormatStrFragment::Fmt {
+                        args: vec![arg],
+                        fmt_str_parts: vec!["".into(), "".into()],
+                    });
+
+                    fragments.push(FormatStrFragment::StyleEnd);
+
+                    s = rest;
+                }
+
+                _ => panic!("bug: at this point, there should be a style tag or styled arg"),
+            }
+        }
+
+        Ok(Self { fragments })
+    }
+}
+
+impl ArgRef {
+    /// (Partially) parses the inside of an format arg (`{...}`). The given
+    /// string `s` must be the inside of the arg and must *not* contain the
+    /// outer braces.
+    pub(crate) fn parse(s: &str) -> Result<Self, Error> {
+        // Split argument reference and format specs.
+        let (arg_str, format_spec) = match s.find(':') {
+            None => (s, ""),
+            Some(colon_pos) => (&s[..colon_pos], &s[colon_pos + 1..]),
+        };
+
+        // Check kind of argument reference.
+        let kind = if arg_str.is_empty() {
+            ArgRefKind::Next
+        } else if let Ok(pos) = arg_str.parse::<usize>() {
+            ArgRefKind::Position(pos)
+        } else {
+            // TODO: make sure the string is a valid Rust identifier
+            ArgRefKind::Name(arg_str.into())
+        };
+
+        let format_spec = FormatSpec::parse(format_spec)?;
+
+        Ok(Self { kind, format_spec })
+    }
+}
+
+impl FormatSpec {
+    /// Parses the format specification that comes after the `:` inside an
+    /// `{...}` argument. The given string must not include the `:` but might be
+    /// empty.
+    pub(crate) fn parse(s: &str) -> Result<Self, Error> {
+        /// Helper iterator for scanning the input
+        struct Peek2<'a> {
+            peek: Option<char>,
+            peek2: Option<char>,
+            rest: Chars<'a>,
+        }
+
+        impl<'a> Peek2<'a> {
+            fn new(s: &'a str) -> Self {
+                let mut rest = s.chars();
+                let peek = rest.next();
+                let peek2 = rest.next();
+
+                Self { peek, peek2, rest }
+            }
+
+            fn next_if<O>(&mut self, f: impl FnOnce(char) -> Option<O>) -> Option<O> {
+                let out = self.peek.and_then(f);
+                if out.is_some() {
+                    self.next();
+                }
+                out
+            }
+
+            fn next_if_eq(&mut self, expected: char) -> bool {
+                if self.peek == Some(expected) {
+                    self.next();
+                    true
+                } else {
+                    false
+                }
+            }
+
+            /// Parses one decimal number and returns whether or not it was
+            /// terminated by `$`.
+            fn parse_num(&mut self) -> Result<(usize, bool), Error> {
+                let mut num: usize = 0;
+                while matches!(self.peek, Some('0'..='9')) {
+                    num = num.checked_mul(10)
+                        .and_then(|num| {
+                            num.checked_add((self.next().unwrap() as u32 - '0' as u32) as usize)
+                        })
+                        .ok_or(err!("width parameter value overflowed `usize`"))?;
+                }
+
+                let arg = self.next_if_eq('$');
+                Ok((num, arg))
+            }
+
+            /// Parses a Rust identifier terminated by '$'. When calling this
+            /// method, `self.peek.map_or(false, |c| c.is_xid_start())` must be
+            /// true.
+            fn parse_ident(&mut self) -> Result<String, Error> {
+                let mut name = String::from(self.next().unwrap());
+                while self.peek.map_or(false, |c| c.is_xid_continue()) {
+                    name.push(self.next().unwrap());
+                }
+
+                if !self.next_if_eq('$') {
+                    return Err(err!(
+                        "invalid format string specification: width/precision named parameter \
+                            does not end with '$'. Note: try the `std` macros to get much better \
+                            error reporting."
+                    ));
+                }
+
+                Ok(name)
+            }
+        }
+
+        impl Iterator for Peek2<'_> {
+            type Item = char;
+            fn next(&mut self) -> Option<Self::Item> {
+                let out = self.peek.take();
+                if out.is_none() {
+                    return None;
+                }
+
+                self.peek = self.peek2;
+                self.peek2 = self.rest.next();
+
+                out
+            }
+        }
+
+
+        let mut it = Peek2::new(s);
+
+        // Fill and align. The former can only exist if the latter also exists.
+        let (fill, align) = if let Some(align) = it.next_if(Align::from_char) {
+            (None, Some(align))
+        } else if let Some(align) = it.peek2.and_then(Align::from_char) {
+            let fill = it.next().unwrap();
+            it.next().unwrap();
+            (Some(fill), Some(align))
+        } else {
+            (None, None)
+        };
+
+        // Simple flags.
+        let sign = it.next_if(Sign::from_char);
+        let alternate = it.next_if_eq('#');
+        let zero = if it.peek == Some('0') && it.peek2 != Some('$') {
+            it.next().unwrap();
+            true
+        } else {
+            false
+        };
+
+        // Width or early exit.
+        let width = match it.peek {
+            // Either a width constant (`8`) or referring to a positional
+            // parameter (`2$`).
+            Some('0'..='9') => {
+                let (num, dollar) = it.parse_num()?;
+
+                if dollar {
+                    Some(Width::Position(num))
+                } else {
+                    Some(Width::Constant(num))
+                }
+            }
+
+            // The "type" (e.g. `?`, `X`) determining the formatting trait. This
+            // means we are done here.
+            Some(c) if it.peek2.is_none() => {
+                return Ok(Self {
+                    fill,
+                    align,
+                    sign,
+                    alternate,
+                    zero,
+                    width: None,
+                    precision: None,
+                    ty: Some(c),
+                });
+            }
+
+            // The start of a `width` named parameter (`foo$`).
+            Some(c) if c.is_xid_start() => Some(Width::Name(it.parse_ident()?)),
+            _ => None,
+        };
+
+        // Precision starting with '.'.
+        let precision = if it.next_if_eq('.') {
+            if it.next_if_eq('*') {
+                Some(Precision::Bundled)
+            } else if matches!(it.peek, Some('0'..='9')) {
+                let (num, dollar) = it.parse_num()?;
+
+                if dollar {
+                    Some(Precision::Position(num))
+                } else {
+                    Some(Precision::Constant(num))
+                }
+            } else {
+                Some(Precision::Name(it.parse_ident()?))
+            }
+        } else {
+            None
+        };
+
+        // Parse type char and make sure nothing else is left.
+        let ty = it.next();
+        if let Some(c) = it.next() {
+            return Err(err!(
+                "expected end of format specification, but found '{}'. Note: use the std \
+                    macros to get much better error reporting.",
+                c,
+            ));
+        }
+
+        Ok(Self {
+            fill,
+            align,
+            sign,
+            alternate,
+            zero,
+            width,
+            precision,
+            ty,
+        })
+    }
+}
+
+impl Align {
+    fn from_char(c: char) -> Option<Self> {
+        match c {
+            '<' => Some(Self::Left),
+            '^' => Some(Self::Center),
+            '>' => Some(Self::Right),
+            _ => None,
+        }
+    }
+}
+
+impl Sign {
+    fn from_char(c: char) -> Option<Self> {
+        match c {
+            '+' => Some(Self::Plus),
+            '-' => Some(Self::Minus),
+            _ => None,
+        }
+    }
+}
diff --git a/macros/src/literal.rs b/macros/src/parse/lit.rs
similarity index 92%
rename from macros/src/literal.rs
rename to macros/src/parse/lit.rs
index 34daaaf..880d84a 100644
--- a/macros/src/literal.rs
+++ b/macros/src/parse/lit.rs
@@ -1,16 +1,27 @@
-//! Just a function to parse a string literal to a `String`. Something that
-//! should be part of `proc-macro`... but ok, we do it ourselves. Of course,
-//! syn also offers that functionality, but we want to avoid that dependency.
-
-use proc_macro2::Literal;
-use super::Error;
-
+use proc_macro2::{
+    Span, TokenTree,
+    token_stream::IntoIter as TokenIterator, Literal,
+};
+use crate::err::Error;
+
+
+/// Tries to parse a string literal.
+pub(super) fn expect_str_literal(it: &mut TokenIterator) -> Result<(String, Span), Error> {
+    match it.next() {
+        Some(TokenTree::Literal(lit)) => Ok((
+            parse_str_literal(&lit)?,
+            lit.span(),
+        )),
+        Some(tt) => {
+            Err(err!(tt.span(), "expected string literal, found different token tree"))
+        }
+        None => Err(err!("expected string literal, found EOF")),
+    }
+}
 
 /// Parses a string literal into the actual string data. Returns an error if the
 /// literal is not a string or raw string literal.
-pub(crate) fn parse_str_literal(lit: &Literal) -> Result<String, Error> {
-    // println!("{:?} -> {}", lit, lit.to_string());
-
+fn parse_str_literal(lit: &Literal) -> Result<String, Error> {
     // In the parsing below, we make use of the fact that the string comes from
     // a `Literal`, i.e. we already know it represents a valid literal. There
     // are only some debug asserts in there to make sure our assumptions are not
@@ -92,7 +103,6 @@ pub(crate) fn parse_str_literal(lit: &Literal) -> Result<String, Error> {
     }
 }
 
-
 #[cfg(test)]
 mod tests {
     use proc_macro2::{TokenStream, TokenTree};
diff --git a/macros/src/parse/mod.rs b/macros/src/parse/mod.rs
new file mode 100644
index 0000000..ae773dd
--- /dev/null
+++ b/macros/src/parse/mod.rs
@@ -0,0 +1,137 @@
+//! Parse the macro input into an intermediate representation.
+
+use proc_macro2::{
+    Span, TokenStream, Delimiter, TokenTree, Spacing,
+    token_stream::IntoIter as TokenIterator,
+};
+use std::collections::HashMap;
+use crate::{
+    err::Error,
+    ir::{Expr, WriteInput, FormatStr, FormatArgs},
+};
+
+mod fmt;
+mod lit;
+mod style;
+
+
+/// Helper function to parse from a token stream. Makes sure the iterator is
+/// empty after `f` returns.
+pub(crate) fn parse<F, O>(tokens: TokenStream, f: F) -> Result<O, Error>
+where
+    F: FnOnce(&mut TokenIterator) -> Result<O, Error>,
+{
+    let mut it = tokens.into_iter();
+    let out = f(&mut it)?;
+
+    if let Some(tt) = it.next() {
+        return Err(err!(tt.span(), "unexpected additional tokens"));
+    }
+
+    Ok(out)
+}
+
+/// Tries to parse a helper group (a group with `None` or `()` delimiter).
+///
+/// These groups are inserted by the declarative macro wrappers in `bunt` to
+/// make parsing in `bunt-macros` easier. In particular, all expressions are
+/// wrapped in these group, allowing us to skip over them without having a Rust
+/// expression parser.
+fn expect_helper_group(tt: Option<TokenTree>) -> Result<(TokenStream, Span), Error> {
+    match tt {
+        Some(TokenTree::Group(g))
+            if g.delimiter() == Delimiter::None || g.delimiter() == Delimiter::Parenthesis =>
+        {
+            Ok((g.stream(), g.span()))
+        }
+        Some(TokenTree::Group(g)) => {
+            Err(err!(
+                g.span(),
+                "expected none or () delimited group, but delimiter is {:?} (note: do not use \
+                    the macros from `bunt-macros` directly, but only through `bunt`)",
+                g.delimiter(),
+            ))
+        }
+        Some(tt) => {
+            Err(err!(
+                tt.span(),
+                "expected none or () delimited group, but found different token tree (note: do \
+                    not use the macros from `bunt-macros` directly, but only through `bunt`)",
+            ))
+        }
+        None => Err(err!("expected none or () delimited group, found EOF")),
+    }
+}
+
+impl WriteInput {
+    pub(crate) fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
+        let target = Expr::parse(it)?;
+        let format_str = FormatStr::parse(it)?;
+        let args = FormatArgs::parse(it)?;
+
+        Ok(Self { target, format_str, args })
+    }
+}
+
+impl Expr {
+    pub(crate) fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
+        let (tokens, span) = expect_helper_group(it.next())?;
+        Ok(Self { tokens, span })
+    }
+}
+
+impl FormatArgs {
+    fn parse(it: &mut TokenIterator) -> Result<Self, Error> {
+        /// Checks if the token stream starting with `tt0` and `tt1` is a named
+        /// argument. If so, returns the name of the argument, otherwise
+        /// (positional argument) returns `None`.
+        fn get_name(tt0: &Option<TokenTree>, tt1: &Option<TokenTree>) -> Option<String> {
+            if let (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(punct))) = (tt0, tt1) {
+                if punct.as_char() == '=' && punct.spacing() == Spacing::Alone {
+                    return Some(name.to_string())
+                }
+            }
+
+            None
+        }
+
+        let mut exprs = Vec::new();
+        let mut name_indices = HashMap::new();
+        let mut saw_named = false;
+
+        // The remaining tokens should all be `None` delimited groups each
+        // representing one argument.
+        for arg_group in it {
+            let (arg, span) = expect_helper_group(Some(arg_group))?;
+            let mut it = arg.into_iter();
+            let tt0 = it.next();
+            let tt1 = it.next();
+
+            if let Some(name) = get_name(&tt0, &tt1) {
+                saw_named = true;
+
+                let expr = Expr {
+                    tokens: it.collect(),
+                    span,
+                };
+                let index = exprs.len();
+                exprs.push(expr);
+                name_indices.insert(name, index);
+            } else {
+                if saw_named {
+                    let e = err!(span, "positional argument after named arguments is not allowed");
+                    return Err(e);
+                }
+
+                let expr = Expr {
+                    tokens: vec![tt0, tt1].into_iter().filter_map(|tt| tt).chain(it).collect(),
+                    span,
+                };
+                exprs.push(expr);
+            }
+
+        }
+
+        Ok(Self { exprs, name_indices })
+    }
+}
diff --git a/macros/src/parse/style.rs b/macros/src/parse/style.rs
new file mode 100644
index 0000000..b71eb6a
--- /dev/null
+++ b/macros/src/parse/style.rs
@@ -0,0 +1,140 @@
+use proc_macro2::{Span, TokenStream};
+use crate::{
+    err::Error,
+    ir::{Style, Color},
+};
+use super::{
+    parse,
+    lit::expect_str_literal,
+};
+
+
+impl Style {
+    /// Parses the style specifiction assuming the token stream contains a
+    /// single string literal.
+    pub(crate) fn parse_from_tokens(tokens: TokenStream) -> Result<Self, Error> {
+        let (s, span) = parse(tokens, expect_str_literal)?;
+        Self::parse(&s, span)
+    }
+
+    /// Parses the style specification in `spec` (with `span`) and returns a token
+    /// stream representing an expression constructing the corresponding `ColorSpec`
+    /// value.
+    pub(super) fn parse(spec: &str, span: Span) -> Result<Self, Error> {
+        let mut out = Self::default();
+
+        let mut previous_fg_color = None;
+        let mut previous_bg_color = None;
+        for fragment in spec.split('+').map(str::trim).filter(|s| !s.is_empty()) {
+            let (fragment, is_bg) = match fragment.strip_prefix("bg:") {
+                Some(color) => (color, true),
+                None => (fragment, false),
+            };
+
+            // Parse/obtain color if a color is specified.
+            let color = match fragment {
+                "black" => Some(Color::Black),
+                "blue" => Some(Color::Blue),
+                "green" => Some(Color::Green),
+                "red" => Some(Color::Red),
+                "cyan" => Some(Color::Cyan),
+                "magenta" => Some(Color::Magenta),
+                "yellow" => Some(Color::Yellow),
+                "white" => Some(Color::White),
+
+                hex if hex.starts_with('#') => {
+                    let hex = &hex[1..];
+
+                    if hex.len() != 6 {
+                        let e = err!(
+                            span,
+                            "hex color code invalid: 6 digits expected, found {}",
+                            hex.len(),
+                        );
+                        return Err(e);
+                    }
+
+                    let digits = hex.chars()
+                        .map(|c| {
+                            c.to_digit(16).ok_or_else(|| {
+                                err!(span, "hex color code invalid: {} is not a valid hex digit", c)
+                            })
+                        })
+                        .collect::<Result<Vec<_>, _>>()?;
+
+                    let r = (digits[0] * 16 + digits[1]) as u8;
+                    let g = (digits[2] * 16 + digits[3]) as u8;
+                    let b = (digits[4] * 16 + digits[5]) as u8;
+
+                    Some(Color::Rgb(r, g, b))
+                },
+
+                // TODO: Ansi256 colors
+                _ => None,
+            };
+
+            // Check for duplicate color definitions.
+            let (previous_color, color_kind) = match is_bg {
+                true => (&mut previous_bg_color, "background"),
+                false => (&mut previous_fg_color, "foreground"),
+            };
+            match (&color, *previous_color) {
+                (Some(_), Some(old)) => {
+                    let e = err!(
+                        span,
+                        "found '{}' but the {} color was already specified as '{}'",
+                        fragment,
+                        color_kind,
+                        old,
+                    );
+                    return Err(e);
+                }
+                (Some(_), None) => *previous_color = Some(fragment),
+                _ => {}
+            }
+
+            macro_rules! set_attr {
+                ($field:ident, $value:expr) => {{
+                    if let Some(b) = out.$field {
+                        let field_s = stringify!($field);
+                        let old = if b { field_s.into() } else { format!("!{}", field_s) };
+                        let new = if $value { field_s.into() } else { format!("!{}", field_s) };
+                        let e = err!(
+                            span,
+                            "invalid style definition: found '{}', but '{}' was specified before",
+                            new,
+                            old,
+                        );
+                        return Err(e);
+                    }
+
+                    out.$field = Some($value);
+                }};
+            }
+
+            // Obtain the final token stream for method call.
+            match (is_bg, color, fragment) {
+                (false, Some(color), _) => out.fg = Some(color),
+                (true, Some(color), _) => out.bg = Some(color),
+                (true, None, other) => {
+                    return Err(err!(span, "'{}' (following 'bg:') is not a valid color", other));
+                }
+
+                (false, None, "bold") => set_attr!(bold, true),
+                (false, None, "!bold") => set_attr!(bold, false),
+                (false, None, "italic") => set_attr!(italic, true),
+                (false, None, "!italic") => set_attr!(italic, false),
+                (false, None, "underline") => set_attr!(underline, true),
+                (false, None, "!underline") => set_attr!(underline, false),
+                (false, None, "intense") => set_attr!(intense, true),
+                (false, None, "!intense") => set_attr!(intense, false),
+
+                (false, None, other) => {
+                    return Err(err!(span, "invalid style spec fragment '{}'", other));
+                }
+            }
+        }
+
+        Ok(out)
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3cfe674..5ef3f89 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -58,10 +58,6 @@
 //! bunt::println!("value: {[green]foo:.5}", foo = 3.14);
 //! ```
 //!
-//! **Note**: Currently, it is not yet possible to refer to parameters for the
-//! "precision" or "width" value. So, `println!("Hello {1:0$}!", 5, "x");"` will
-//! fail to compile right now.
-//!
 //! ## Style specification
 //!
 //! `bunt` has the same capabilities as `termcolor`. See [`termcolor::Color`]


Test patch:
diff --git a/macros/src/tests.rs b/macros/src/tests.rs
new file mode 100644
index 0000000..280bd7b
--- /dev/null
+++ b/macros/src/tests.rs
@@ -0,0 +1,149 @@
+use crate::ir::{Align, Width, Sign, FormatSpec, Precision};
+
+
+#[track_caller]
+fn check_format_spec(expected_str: &str, expected_parsed: FormatSpec) {
+    let actual_parsed = FormatSpec::parse(expected_str)
+        .expect(&format!("failed to parse format spec '{}'", expected_str));
+
+    if actual_parsed != expected_parsed {
+        panic!(
+            "result of parsing format spec '{}' unexpected.\
+                \n  expected: {:?}\
+                \n    actual: {:?}\n",
+            expected_str,
+            expected_parsed,
+            actual_parsed,
+        );
+    }
+
+    let actual_string = actual_parsed.to_string();
+    if actual_string != expected_str {
+        panic!(
+            "result of stringifying '{:?}' unexpected.\
+                \n  expected: {}\
+                \n    actual: {}\n",
+            actual_parsed,
+            expected_str,
+            actual_string,
+        );
+    }
+}
+
+#[test]
+fn format_spec_simple() {
+    check_format_spec("", FormatSpec::default());
+    check_format_spec("x", FormatSpec {
+        ty: Some('x'),
+        .. FormatSpec::default()
+    });
+    check_format_spec("<", FormatSpec {
+        align: Some(Align::Left),
+        .. FormatSpec::default()
+    });
+    check_format_spec("x^", FormatSpec {
+        fill: Some('x'),
+        align: Some(Align::Center),
+        .. FormatSpec::default()
+    });
+    check_format_spec("+", FormatSpec {
+        sign: Some(Sign::Plus),
+        .. FormatSpec::default()
+    });
+    check_format_spec("#", FormatSpec {
+        alternate: true,
+        .. FormatSpec::default()
+    });
+    check_format_spec("0", FormatSpec {
+        zero: true,
+        .. FormatSpec::default()
+    });
+}
+
+#[test]
+fn format_spec_width() {
+    check_format_spec("5", FormatSpec {
+        width: Some(Width::Constant(5)),
+        .. FormatSpec::default()
+    });
+    check_format_spec("0$", FormatSpec {
+        width: Some(Width::Position(0)),
+        .. FormatSpec::default()
+    });
+    check_format_spec("2$", FormatSpec {
+        width: Some(Width::Position(2)),
+        .. FormatSpec::default()
+    });
+    check_format_spec("peter$", FormatSpec {
+        width: Some(Width::Name("peter".into())),
+        .. FormatSpec::default()
+    });
+}
+
+#[test]
+fn format_spec_precision() {
+    check_format_spec(".5", FormatSpec {
+        precision: Some(Precision::Constant(5)),
+        .. FormatSpec::default()
+    });
+    check_format_spec(".2$", FormatSpec {
+        precision: Some(Precision::Position(2)),
+        .. FormatSpec::default()
+    });
+    check_format_spec(".peter$", FormatSpec {
+        precision: Some(Precision::Name("peter".into())),
+        .. FormatSpec::default()
+    });
+    check_format_spec(".*", FormatSpec {
+        precision: Some(Precision::Bundled),
+        .. FormatSpec::default()
+    });
+}
+
+#[test]
+fn format_spec_mixed() {
+    check_format_spec("1.0", FormatSpec {
+        width: Some(Width::Constant(1)),
+        precision: Some(Precision::Constant(0)),
+        .. FormatSpec::default()
+    });
+    check_format_spec("1$.peter$", FormatSpec {
+        width: Some(Width::Position(1)),
+        precision: Some(Precision::Name("peter".into())),
+        .. FormatSpec::default()
+    });
+    check_format_spec("04", FormatSpec {
+        zero: true,
+        width: Some(Width::Constant(4)),
+        .. FormatSpec::default()
+    });
+    check_format_spec("-<5", FormatSpec {
+        fill: Some('-'),
+        align: Some(Align::Left),
+        width: Some(Width::Constant(5)),
+        .. FormatSpec::default()
+    });
+    check_format_spec("00", FormatSpec {
+        zero: true,
+        width: Some(Width::Constant(0)),
+        .. FormatSpec::default()
+    });
+    check_format_spec("#010x", FormatSpec {
+        alternate: true,
+        zero: true,
+        width: Some(Width::Constant(10)),
+        ty: Some('x'),
+        .. FormatSpec::default()
+    });
+
+    check_format_spec("_>+#0x$.*?", FormatSpec {
+        fill: Some('_'),
+        align: Some(Align::Right),
+        sign: Some(Sign::Plus),
+        alternate: true,
+        zero: true,
+        width: Some(Width::Name("x".into())),
+        precision: Some(Precision::Bundled),
+        ty: Some('?'),
+    });
+}
diff --git a/tests/main.rs b/tests/main.rs
index 3a678cc..e75bc25 100644
--- a/tests/main.rs
+++ b/tests/main.rs
@@ -119,6 +119,39 @@ fn arg_referal() {
         "a p c b a m" ==
         "{} {peter} {2} {} {0} {mary}", 'a', 'b', 'c', peter = 'p', mary = 'm'
     );
+
+    check!("3 5" == "{} {}", foo = 3, bar = 5);
+    check!("3 7 5 3 5 7" == "{} {baz} {} {0} {bar} {}", foo = 3, bar = 5, baz = 7);
+}
+
+#[test]
+fn arg_referal_width() {
+    check!("10|foo       |true" == "{}|{:0$}|{}", 10, "foo", true);
+    check!("10|foo    |true" == "{}|{:3$}|{}", 10, "foo", true, 7);
+    check!("10|foo    |true" == "{}|{:wid$}|{}", 10, "foo", true, wid = 7);
+    check!("10|foo    |7" == "{}|{:wid$}|{wid}", 10, "foo", wid = 7);
+
+    check!("bar|    5|5" == "{}|{1:1$}|{}", "bar", 5);
+    check!("bar|anna |5" == "{}|{name:1$}|{}", "bar", 5, name = "anna");
+    check!("bar|        5|5" == "{}|{1:wid$}|{}", "bar", 5, wid = 9);
+    check!("bar|anna     |5" == "{}|{name:wid$}|{}", "bar", 5, wid = 9, name = "anna");
+}
+
+#[test]
+fn arg_referal_precision() {
+    check!("2|3.14|true" == "{}|{:.0$}|{}", 2, 3.1415926, true);
+    check!("2|3.142|true" == "{}|{:.3$}|{}", 2, 3.1415926, true, 3);
+    check!("2|3.142|true" == "{}|{:.prec$}|{}", 2, 3.1415926, true, prec = 3);
+    check!("2|3.142|3" == "{}|{:.prec$}|{prec}", 2, 3.1415926, prec = 3);
+
+    check!("bar|3.1|1" == "{}|{2:.1$}|{}", "bar", 1, 3.1415926);
+    check!("bar|3.1|1" == "{}|{pi:.1$}|{}", "bar", 1, pi = 3.1415926);
+    check!("bar|3.1|1" == "{}|{pi:.prec$}|{}", "bar", 1, pi = 3.1415926, prec = 1);
+    check!("bar|3.1|1" == "{}|{2:.prec$}|{}", "bar", 1, 3.1415926, prec = 1);
+
+    check!("foo|3.14|true" == "{}|{:.*}|{}", "foo", 2, 3.1415926, true);
+    check!("3.1415926|3.14|foo" == "{}|{0:.*}|{}", 3.1415926, 2, "foo");
+    check!("true|3.14|foo" == "{}|{pi:.*}|{}", true, 2, "foo", pi = 3.1415926);
 }
 
 #[test]