Repository: LukasKalbertodt/bunt (language: rust)

Issue:
Research how to get rid of `syn` with `full` feature dependency
This adds quote a bit of compile time. I don't even need Rust AST types or anything. The only reason for syn is to parse an expression delimited by a comma. I.e. `$foo:expr ,` in declarative macro syntax. But I don't really inspect the expression and just treat it as token stream. It's not trivial to parse that yourself, however. Take `write!(foo(a, b), "..")`: you can't stop after the first comma.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d8cbad..a1a7b44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,12 +4,18 @@ All notable changes to this project will be documented in this file.
 
 
 ## [Unreleased]
+### Breaking changes
+- Minimal Rust version bumped to 1.46.0
+
+### Changed
+- `syn` dependency removed
 
 
 ## [0.1.1] - 2020-09-05
 ### Fixed
 - Minor documentation fixes
 
+
 ## 0.1.0 - 2020-07-30
 ### Added
 - Everything (`write`, `writeln`, `print`, `println`, `style`)
diff --git a/README.md b/README.md
index 94aad7d..63e4c4b 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@ Bunt
 
 `bunt` offers macros to easily print colored and formatted text to a terminal.
 It is just a convenience API on top of [`termcolor`](https://crates.io/crates/termcolor).
+`bunt` is implemented using procedural macros, but as it does not depend on `syn`, compilation is fairly quick (≈1.5s on my machine, including all dependencies).
 
 ```rust
 // Style tags will color/format text between the tags.
@@ -27,7 +28,8 @@ See [**the documentation**](https://docs.rs/bunt) for more information.
 
 ## Status of this project
 
-Very young project. Syntax is by no means final yet.
+This is still a young project, but I already use it in two applications of mine.
+The syntax is certainly not final yet.
 [Seeking feedback from the community!](https://github.com/LukasKalbertodt/bunt/issues/1)
 
 
diff --git a/macros/Cargo.toml b/macros/Cargo.toml
index fb9b0fe..de82405 100644
--- a/macros/Cargo.toml
+++ b/macros/Cargo.toml
@@ -13,4 +13,3 @@ proc-macro = true
 [dependencies]
 quote = "1"
 proc-macro2 = "1.0.19"
-syn = { version = "1", features = ["full", "extra-traits"] }
diff --git a/macros/src/err.rs b/macros/src/err.rs
new file mode 100644
index 0000000..d56d1f7
--- /dev/null
+++ b/macros/src/err.rs
@@ -0,0 +1,26 @@
+use proc_macro2::{Span, TokenStream};
+use quote::quote_spanned;
+
+
+/// Helper macro to easily create an error with a span.
+macro_rules! err {
+    ($fmt:literal $($t:tt)*) => { Error { span: Span::call_site(), msg: format!($fmt $($t)*) } };
+    ($span:expr, $($t:tt)+) => { Error { span: $span, msg: format!($($t)+) } };
+}
+
+/// Simply contains a message and a span. Can be converted to a `compile_error!`
+/// via `to_compile_error`.
+#[derive(Debug)]
+pub(crate) struct Error {
+    pub(crate) msg: String,
+    pub(crate) span: Span,
+}
+
+impl Error {
+    pub(crate) fn to_compile_error(&self) -> TokenStream {
+        let msg = &self.msg;
+        quote_spanned! {self.span=>
+            compile_error!(#msg);
+        }
+    }
+}
diff --git a/macros/src/gen.rs b/macros/src/gen.rs
new file mode 100644
index 0000000..8b48706
--- /dev/null
+++ b/macros/src/gen.rs
@@ -0,0 +1,229 @@
+//! Generating the output tokens from the parsed intermediate representation.
+
+use proc_macro2::{Ident, Span, TokenStream};
+use quote::quote;
+use std::{
+    collections::BTreeSet,
+    fmt::Write,
+};
+use crate::{
+    err::Error,
+    ir::{WriteInput, FormatStrFragment, ArgRefKind, Style, Color, Expr},
+};
+
+
+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())
+        }
+
+        // Create a binding for each given argument. This is useful for two
+        // reasons:
+        // - The given expression could have side effects or be compuationally
+        //   expensive. The formatting macros from std guarantee that the
+        //   expression is evaluated only once, so we want to guarantee the
+        //   same.
+        // - We can then very easily refer to all arguments later. Without these
+        //   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);
+            arg_bindings.extend(quote! {
+                let #ident = &#arg;
+            })
+        }
+
+        // Prepare the actual process of writing to the target according to the
+        // format string.
+        let buf = Ident::new("buf", Span::mixed_site());
+        let mut style_stack = Vec::new();
+        let mut writes = TokenStream::new();
+        let mut next_arg_index = 0;
+
+        for segment in &self.format_str.fragments {
+            match segment {
+                // A formatting fragment. This is the more tricky one. We have
+                // to construct a `std::write!` invocation that has the right
+                // fmt string, the right arguments (and no additional ones!) and
+                // the correct argument references.
+                FormatStrFragment::Fmt { fmt_str_parts, args } => {
+                    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)")
+                                    );
+                                }
+
+                                next_arg_index += 1;
+                                ident
+                            }
+                            ArgRefKind::Position(pos) => {
+                                let ident = pos_arg_ident(*pos);
+                                if self.args.positional.get(*pos as usize).is_none() {
+                                    return Err(err!(
+                                        "invalid reference to positional argument {} (there are \
+                                            not that many arguments)",
+                                        pos,
+                                    ));
+                                }
+
+                                ident
+                            }
+                            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));
+                                }
+
+                                ident
+                            }
+                        };
+
+                        std::write!(fmt_str, "{{{}{}}}", ident, arg.format_spec).unwrap();
+                        used_args.insert(ident);
+                        fmt_str.push_str(&fmt_str_parts[i + 1]);
+                    }
+
+
+                    // Combine everything in `write!` invocation.
+                    writes.extend(quote! {
+                        std::write!(#buf, #fmt_str #(, #used_args = #used_args)* )?;
+                    });
+                }
+
+                // A style start tag: we simply create the `ColorSpec` and call
+                // `set_color`. The interesting part is how the styles stack and
+                // merge.
+                FormatStrFragment::StyleStart(style) => {
+                    let last_style = style_stack.last().copied().unwrap_or(Style::default());
+                    let new_style = style.or(last_style);
+                    let style_def = new_style.to_tokens();
+                    style_stack.push(new_style);
+                    writes.extend(quote! {
+                        ::bunt::termcolor::WriteColor::set_color(#buf, &#style_def)?;
+                    });
+                }
+
+                // Revert the last style tag. This means that we pop the topmost
+                // style from the stack and apply the *then* topmost style
+                // again.
+                FormatStrFragment::StyleEnd => {
+                    style_stack.pop().ok_or(err!("unmatched closing style tag"))?;
+                    let style = style_stack.last().copied().unwrap_or(Style::default());
+                    let style_def = style.to_tokens();
+                    writes.extend(quote! {
+                        ::bunt::termcolor::WriteColor::set_color(#buf, &#style_def)?;
+                    });
+                }
+            }
+        }
+
+        // Check if the style tags are balanced
+        if !style_stack.is_empty() {
+            return Err(err!("unclosed style tag"));
+        }
+
+        // Combine everything.
+        let target = &self.target;
+        Ok(quote! {
+            (|| -> Result<(), ::std::io::Error> {
+                use std::io::Write as _;
+
+                #arg_bindings
+                let #buf = &mut #target;
+                #writes
+
+                Ok(())
+            })()
+        })
+    }
+}
+
+impl quote::ToTokens for Expr {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(self.tokens.clone())
+    }
+}
+
+impl Style {
+    /// Returns a token stream representing an expression constructing the
+    /// `ColorSpec` value corresponding to `self`.
+    pub(crate) fn to_tokens(&self) -> TokenStream {
+        let ident = Ident::new("color_spec", Span::mixed_site());
+        let mut method_calls = TokenStream::new();
+
+        if let Some(fg) = self.fg {
+            let fg = fg.to_tokens();
+            method_calls.extend(quote! {
+                #ident.set_fg(Some(#fg));
+            })
+        }
+        if let Some(bg) = self.bg {
+            let bg = bg.to_tokens();
+            method_calls.extend(quote! {
+                #ident.set_bg(Some(#bg));
+            })
+        }
+
+        macro_rules! attr {
+            ($field:ident, $method:ident) => {
+                if let Some(b) = self.$field {
+                    method_calls.extend(quote! {
+                        #ident.$method(#b);
+                    });
+                }
+            };
+        }
+
+        attr!(bold, set_bold);
+        attr!(italic, set_italic);
+        attr!(underline, set_underline);
+        attr!(intense, set_intense);
+
+        quote! {
+            {
+                let mut #ident = ::bunt::termcolor::ColorSpec::new();
+                #method_calls
+                #ident
+            }
+        }
+    }
+}
+
+impl Color {
+    /// Returns a token stream representing a value of type `termcolor::Color`.
+    fn to_tokens(&self) -> TokenStream {
+        let variant = match self {
+            Self::Black => Some(quote! { Black }),
+            Self::Blue => Some(quote! { Blue }),
+            Self::Green => Some(quote! { Green }),
+            Self::Red => Some(quote! { Red }),
+            Self::Cyan => Some(quote! { Cyan }),
+            Self::Magenta => Some(quote! { Magenta }),
+            Self::Yellow => Some(quote! { Yellow }),
+            Self::White => Some(quote! { White }),
+            Self::Rgb(r, g, b) => Some(quote! { Rgb(#r, #g, #b) }),
+        };
+
+        quote! { ::bunt::termcolor::Color:: #variant }
+    }
+}
diff --git a/macros/src/ir.rs b/macros/src/ir.rs
new file mode 100644
index 0000000..3ed0aa2
--- /dev/null
+++ b/macros/src/ir.rs
@@ -0,0 +1,139 @@
+//! Types for the intermediate representation of the macro input. This parsed
+//! representation allows the functions in `gen.rs` to work more easily.
+
+use proc_macro2::{Span, TokenStream};
+use std::collections::HashMap;
+
+
+/// Input for the `write!` and `writeln!` macro.
+#[derive(Debug)]
+pub(crate) struct WriteInput {
+    pub(crate) target: Expr,
+    pub(crate) format_str: FormatStr,
+    pub(crate) args: FormatArgs,
+}
+
+/// Our own `expr` type. We use this instead of `syn` to avoid `syn`
+/// alltogether. We don't need to introspect the expression, we just need to
+/// skip over them and the emit them again.
+#[derive(Debug)]
+pub(crate) struct Expr {
+    pub(crate) span: Span,
+    pub(crate) tokens: TokenStream,
+}
+
+/// A parsed format string.
+#[derive(Debug)]
+pub(crate) struct FormatStr {
+    pub(crate) fragments: Vec<FormatStrFragment>,
+}
+
+impl FormatStr {
+    /// Adds `\n` to the end of the formatting string.
+    pub(crate) fn add_newline(&mut self) {
+        match self.fragments.last_mut() {
+            // If the last fragment is an `fmt` one, we can easily add the
+            // newline to its last part (which is guaranteed to exist).
+            Some(FormatStrFragment::Fmt { fmt_str_parts, .. }) => {
+                fmt_str_parts.last_mut()
+                    .expect("bug: fmt_str_parts empty")
+                    .push('\n');
+            }
+
+            // Otherwise (style closing tag is last fragment), we have to add a
+            // new `Fmt` fragment.
+            _ => {
+                self.fragments.push(FormatStrFragment::Fmt {
+                    fmt_str_parts: vec!["\n".into()],
+                    args: vec![],
+                });
+            }
+        }
+    }
+}
+
+/// One fragment of the format string.
+#[derive(Debug)]
+pub(crate) enum FormatStrFragment {
+    /// A format string without style tags, but potentially with arguments.
+    ///
+    /// `fmt_str_parts` always has exactly one element more than `args`.
+    Fmt {
+        /// The format string as parts between the arguments.
+        fmt_str_parts: Vec<String>,
+
+        /// Information about argument that are referenced.
+        args: Vec<ArgRef>,
+    },
+
+    /// A `{$...}` style start tag.
+    StyleStart(Style),
+
+    /// A `{/$}` style end tag.
+    StyleEnd,
+}
+
+#[derive(Debug)]
+pub(crate) struct ArgRef {
+    pub(crate) kind: ArgRefKind,
+    pub(crate) format_spec: String,
+}
+
+/// How a format argument is referred to.
+#[derive(Debug)]
+pub(crate) enum ArgRefKind {
+    /// `{}`
+    Next,
+    /// `{2}`
+    Position(u32),
+    /// `{peter}`
+    Name(String),
+}
+
+/// Parsed formatting arguments.
+#[derive(Debug)]
+pub(crate) struct FormatArgs {
+    pub(crate) positional: Vec<Expr>,
+    pub(crate) named: HashMap<String, Expr>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum Color {
+    Black,
+    Blue,
+    Green,
+    Red,
+    Cyan,
+    Magenta,
+    Yellow,
+    White,
+    //Ansi256(u8), // TODO: add
+    Rgb(u8, u8, u8),
+}
+
+#[derive(Debug, Default, Clone, Copy)]
+pub(crate) struct Style {
+    pub(crate) fg: Option<Color>,
+    pub(crate) bg: Option<Color>,
+    pub(crate) bold: Option<bool>,
+    pub(crate) intense: Option<bool>,
+    pub(crate) underline: Option<bool>,
+    pub(crate) italic: Option<bool>,
+    pub(crate) reset: Option<bool>,
+}
+
+impl Style {
+    /// Like `Option::or`: all style values set in `self` are kept, all unset
+    /// ones are overwritten with the values from `style_b`.
+    pub(crate) fn or(&self, style_b: Self) -> Self {
+        Self {
+            fg: self.fg.or(style_b.fg),
+            bg: self.bg.or(style_b.bg),
+            bold: self.bold.or(style_b.bold),
+            intense: self.intense.or(style_b.intense),
+            underline: self.underline.or(style_b.underline),
+            italic: self.italic.or(style_b.italic),
+            reset: self.reset.or(style_b.reset),
+        }
+    }
+}
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index fde5b79..7b0d442 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -2,33 +2,26 @@
 //! detail, please see the crate `bunt` for the real docs.
 
 use proc_macro::TokenStream as TokenStream1;
-use proc_macro2::{Ident, Span, TokenStream};
-use quote::quote;
-use syn::{
-    Error,
-    LitStr,
-    Token,
-    parse::{Parse, ParseStream},
-    spanned::Spanned,
+use proc_macro2::TokenStream;
+
+#[macro_use]
+mod err;
+mod gen;
+mod ir;
+mod literal;
+mod parse;
+
+use crate::{
+    err::Error,
+    ir::{Style, WriteInput},
 };
-use std::{
-    collections::{BTreeSet, HashMap},
-    fmt::Write,
-};
-
 
-/// Helper macro to easily create an error with a span.
-macro_rules! err {
-    ($fmt:literal $($t:tt)*) => { syn::Error::new(Span::call_site(), format!($fmt $($t)*)) };
-    ($span:expr, $($t:tt)+) => { syn::Error::new($span, format!($($t)+)) };
-}
 
 // Docs are in the `bunt` reexport.
 #[proc_macro]
 pub fn style(input: TokenStream1) -> TokenStream1 {
     run(input, |input| {
-        let literal = syn::parse2::<LitStr>(input)?;
-        let style = Style::parse(&literal.value(), literal.span())?;
+        let style = Style::parse_from_tokens(input)?;
         Ok(style.to_tokens())
     })
 }
@@ -36,565 +29,19 @@ pub fn style(input: TokenStream1) -> TokenStream1 {
 // Docs are in the `bunt` reexport.
 #[proc_macro]
 pub fn write(input: TokenStream1) -> TokenStream1 {
-    run(input, |input| syn::parse2::<WriteInput>(input)?.gen_output())
+    run(input, |input| parse::parse(input, WriteInput::parse)?.gen_output())
 }
 
 // Docs are in the `bunt` reexport.
 #[proc_macro]
 pub fn writeln(input: TokenStream1) -> TokenStream1 {
     run(input, |input| {
-        let mut input = syn::parse2::<WriteInput>(input)?;
+        let mut input = parse::parse(input, WriteInput::parse)?;
         input.format_str.add_newline();
         input.gen_output()
     })
 }
 
-// Docs are in the `bunt` reexport.
-#[proc_macro]
-pub fn print(input: TokenStream1) -> TokenStream1 {
-    run(input, |input| {
-        let out = syn::parse2::<PrintInput>(input)?.gen_output()?;
-        Ok(quote! {
-            #out.expect("failed to write to stdout in `bunt::print`")
-        })
-    })
-}
-
-// Docs are in the `bunt` reexport.
-#[proc_macro]
-pub fn println(input: TokenStream1) -> TokenStream1 {
-    run(input, |input| {
-        let mut input = syn::parse2::<PrintInput>(input)?;
-        input.format_str.add_newline();
-        let out = input.gen_output()?;
-
-        Ok(quote! {
-            #out.expect("failed to write to stdout in `bunt::println`")
-        })
-    })
-}
-
-/// Input for the `write!` and `writeln!` macro. Also used by other convenience
-/// macros.
-#[derive(Debug)]
-struct WriteInput {
-    target: syn::Expr,
-    format_str: FormatStr,
-    args: FormatArgs,
-}
-
-impl WriteInput {
-    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())
-        }
-
-        // Create a binding for each given argument. This is useful for two
-        // reasons:
-        // - The given expression could have side effects or be compuationally
-        //   expensive. The formatting macros from std guarantee that the
-        //   expression is evaluated only once, so we want to guarantee the
-        //   same.
-        // - We can then very easily refer to all arguments later. Without these
-        //   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);
-            arg_bindings.extend(quote! {
-                let #ident = &#arg;
-            })
-        }
-
-        // Prepare the actual process of writing to the target according to the
-        // format string.
-        let buf = Ident::new("buf", Span::mixed_site());
-        let mut style_stack = Vec::new();
-        let mut writes = TokenStream::new();
-        let mut next_arg_index = 0;
-
-        for segment in &self.format_str.fragments {
-            match segment {
-                // A formatting fragment. This is the more tricky one. We have
-                // to construct a `std::write!` invocation that has the right
-                // fmt string, the right arguments (and no additional ones!) and
-                // the correct argument references.
-                FormatStrFragment::Fmt { fmt_str_parts, args } => {
-                    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)")
-                                    );
-                                }
-
-                                next_arg_index += 1;
-                                ident
-                            }
-                            ArgRefKind::Position(pos) => {
-                                let ident = pos_arg_ident(*pos);
-                                if self.args.positional.get(*pos as usize).is_none() {
-                                    return Err(err!(
-                                        "invalid reference to positional argument {} (there are \
-                                            not that many arguments)",
-                                        pos,
-                                    ));
-                                }
-
-                                ident
-                            }
-                            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));
-                                }
-
-                                ident
-                            }
-                        };
-
-                        std::write!(fmt_str, "{{{}{}}}", ident, arg.format_spec).unwrap();
-                        used_args.insert(ident);
-                        fmt_str.push_str(&fmt_str_parts[i + 1]);
-                    }
-
-
-                    // Combine everything in `write!` invocation.
-                    writes.extend(quote! {
-                        std::write!(#buf, #fmt_str #(, #used_args = #used_args)* )?;
-                    });
-                }
-
-                // A style start tag: we simply create the `ColorSpec` and call
-                // `set_color`. The interesting part is how the styles stack and
-                // merge.
-                FormatStrFragment::StyleStart(style) => {
-                    let last_style = style_stack.last().copied().unwrap_or(Style::default());
-                    let new_style = style.or(last_style);
-                    let style_def = new_style.to_tokens();
-                    style_stack.push(new_style);
-                    writes.extend(quote! {
-                        ::bunt::termcolor::WriteColor::set_color(#buf, &#style_def)?;
-                    });
-                }
-
-                // Revert the last style tag. This means that we pop the topmost
-                // style from the stack and apply the *then* topmost style
-                // again.
-                FormatStrFragment::StyleEnd => {
-                    style_stack.pop().ok_or(err!("unmatched closing style tag"))?;
-                    let style = style_stack.last().copied().unwrap_or(Style::default());
-                    let style_def = style.to_tokens();
-                    writes.extend(quote! {
-                        ::bunt::termcolor::WriteColor::set_color(#buf, &#style_def)?;
-                    });
-                }
-            }
-        }
-
-        // Check if the style tags are balanced
-        if !style_stack.is_empty() {
-            return Err(err!("unclosed style tag"));
-        }
-
-        // Combine everything.
-        let target = &self.target;
-        Ok(quote! {
-            (|| -> Result<(), ::std::io::Error> {
-                use std::io::Write as _;
-
-                #arg_bindings
-                let #buf = &mut #target;
-                #writes
-
-                Ok(())
-            })()
-        })
-    }
-}
-
-impl Parse for WriteInput {
-    fn parse(input: ParseStream) -> Result<Self, Error> {
-        let target = input.parse()?;
-        input.parse::<Token![,]>()?;
-        let format_str = input.parse()?;
-        let args = input.parse()?;
-
-        Ok(Self { target, format_str, args })
-    }
-}
-
-/// Input for the `print!` and `println!` macro.
-#[derive(Debug)]
-struct PrintInput {
-    format_str: FormatStr,
-    args: FormatArgs,
-}
-
-impl PrintInput {
-    fn gen_output(self) -> Result<TokenStream, Error> {
-        let target = syn::parse2(quote! {
-            ::bunt::termcolor::StandardStream::stdout(::bunt::termcolor::ColorChoice::Auto)
-        }).expect("bug: could not parse print target expr");
-
-        let wi = WriteInput {
-            target,
-            format_str: self.format_str,
-            args: self.args,
-        };
-
-        wi.gen_output()
-    }
-}
-
-impl Parse for PrintInput {
-    fn parse(input: ParseStream) -> Result<Self, Error> {
-        let format_str = input.parse()?;
-        let args = input.parse()?;
-
-        Ok(Self { format_str, args })
-    }
-}
-
-/// One fragment of the format string.
-#[derive(Debug)]
-enum FormatStrFragment {
-    /// A format string without style tags, but potentially with arguments.
-    ///
-    /// `fmt_str_parts` always has exactly one element more than `args`.
-    Fmt {
-        /// The format string as parts between the arguments.
-        fmt_str_parts: Vec<String>,
-
-        /// Information about argument that are referenced.
-        args: Vec<ArgRef>,
-    },
-
-    /// A `{$...}` style start tag.
-    StyleStart(Style),
-
-    /// A `{/$}` style end tag.
-    StyleEnd,
-}
-
-#[derive(Debug)]
-struct ArgRef {
-    kind: ArgRefKind,
-    format_spec: String,
-}
-
-/// How a format argument is referred to.
-#[derive(Debug)]
-enum ArgRefKind {
-    /// `{}`
-    Next,
-    /// `{2}`
-    Position(u32),
-    /// `{peter}`
-    Name(String),
-}
-
-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.
-    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 {
-            syn::parse_str::<syn::Ident>(arg_str)?;
-            ArgRefKind::Name(arg_str.into())
-        };
-
-        Ok(Self { kind, format_spec: format_spec.into() })
-    }
-}
-
-/// A parsed format string.
-#[derive(Debug)]
-struct FormatStr {
-    fragments: Vec<FormatStrFragment>,
-}
-
-impl FormatStr {
-    /// Adds `\n` to the end of the formatting string.
-    fn add_newline(&mut self) {
-        match self.fragments.last_mut() {
-            // If the last fragment is an `fmt` one, we can easily add the
-            // newline to its last part (which is guaranteed to exist).
-            Some(FormatStrFragment::Fmt { fmt_str_parts, .. }) => {
-                fmt_str_parts.last_mut()
-                    .expect("bug: fmt_str_parts empty")
-                    .push('\n');
-            }
-
-            // Otherwise (style closing tag is last fragment), we have to add a
-            // new `Fmt` fragment.
-            _ => {
-                self.fragments.push(FormatStrFragment::Fmt {
-                    fmt_str_parts: vec!["\n".into()],
-                    args: vec![],
-                });
-            }
-        }
-    }
-}
-
-impl Parse for FormatStr {
-    fn parse(input: ParseStream) -> 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 lit = input.parse::<syn::LitStr>()?;
-        let raw = lit.value();
-
-        // 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..], lit.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..], lit.span())?;
-                    let style = Style::parse(inner, lit.span())?;
-                    fragments.push(FormatStrFragment::StyleStart(style));
-                    s = rest;
-                }
-
-                () if s.starts_with("{[") => {
-                    let (inner, rest) = split_at_closing_brace(&s[1..], lit.span())?;
-
-                    // Parse style information
-                    let style_end = inner.find(']')
-                        .ok_or(err!(lit.span(), "unclosed '[' in format string argument"))?;
-                    let style = Style::parse(&inner[1..style_end], lit.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 })
-    }
-}
-
-/// Parsed formatting arguments.
-#[derive(Debug)]
-struct FormatArgs {
-    positional: Vec<syn::Expr>,
-    named: HashMap<String, syn::Expr>,
-}
-
-impl Parse for FormatArgs {
-    fn parse(input: ParseStream) -> Result<Self, Error> {
-        let mut positional = Vec::new();
-        let mut named = HashMap::new();
-        let mut saw_named = false;
-
-        // We expect a comma here as this is always following the format string.
-        if !input.peek(Token![,]) {
-            return Ok(Self { positional, named })
-        }
-        input.parse::<Token![,]>()?;
-
-        loop {
-            if input.is_empty() {
-                break;
-            }
-
-            // Parse the argument.
-            match input.parse()? {
-                FormatArg::Positional(e) => {
-                    if saw_named {
-                        let e = err!(
-                            e.span(),
-                            "positional argument after named arguments is not allowed",
-                        );
-                        return Err(e);
-                    }
-
-                    positional.push(e);
-                },
-                FormatArg::Named(name, e) => {
-                    saw_named = true;
-                    named.insert(name, e);
-                }
-            }
-
-            // Consume comma or stop.
-            if !input.peek(Token![,]) {
-                break;
-            }
-            input.parse::<Token![,]>()?;
-        }
-
-        Ok(Self { positional, named })
-    }
-}
-
-/// A single format argument.
-#[derive(Debug)]
-enum FormatArg {
-    /// This argument is not named, e.g. just `27`.
-    Positional(syn::Expr),
-    /// A named argument, e.g. `value = 27`.
-    Named(String, syn::Expr),
-}
-
-impl Parse for FormatArg {
-    fn parse(input: ParseStream) -> Result<Self, Error> {
-        match input.parse()? {
-            syn::Expr::Assign(syn::ExprAssign { attrs, left, right, .. }) => {
-                if let Some(attr) = attrs.get(0) {
-                    return Err(err!(attr.span(), "attributes invalid in this context"));
-                }
-
-                // We only accept a single identifier on the left.
-                match *left {
-                    syn::Expr::Path(path) => {
-                        let ident = path.path.get_ident();
-                        if !path.attrs.is_empty() || path.qself.is_some() || ident.is_none() {
-                            let e = err!(
-                                path.span(),
-                                "expected single identifier, found path on the left \
-                                    side of the '=' in named parameter",
-                            );
-                            return Err(e);
-                        }
-
-                        Ok(Self::Named(ident.unwrap().to_string(), *right))
-                    }
-                    other => {
-                        let e = err!(
-                            other.span(),
-                            "expected single identifier, found some expression on the left \
-                                side of the '=' in named parameter",
-                        );
-                        return Err(e);
-                    }
-                }
-            }
-
-            // TODO: maybe disallow some expression types
-
-            expr => Ok(Self::Positional(expr)),
-        }
-    }
-}
-
-
 /// Performs the conversion from and to `proc_macro::TokenStream` and converts
 /// `Error`s into `compile_error!` tokens.
 fn run(
@@ -605,227 +52,3 @@ fn run(
         .unwrap_or_else(|e| e.to_compile_error())
         .into()
 }
-
-
-#[derive(Debug, Clone, Copy)]
-enum Color {
-    Black,
-    Blue,
-    Green,
-    Red,
-    Cyan,
-    Magenta,
-    Yellow,
-    White,
-    //Ansi256(u8), // TODO: add
-    Rgb(u8, u8, u8),
-}
-
-impl Color {
-    fn to_tokens(&self) -> TokenStream {
-        let variant = match self {
-            Self::Black => Some(quote! { Black }),
-            Self::Blue => Some(quote! { Blue }),
-            Self::Green => Some(quote! { Green }),
-            Self::Red => Some(quote! { Red }),
-            Self::Cyan => Some(quote! { Cyan }),
-            Self::Magenta => Some(quote! { Magenta }),
-            Self::Yellow => Some(quote! { Yellow }),
-            Self::White => Some(quote! { White }),
-            Self::Rgb(r, g, b) => Some(quote! { Rgb(#r, #g, #b) }),
-        };
-
-        quote! { ::bunt::termcolor::Color:: #variant }
-    }
-}
-
-#[derive(Debug, Default, Clone, Copy)]
-struct Style {
-    fg: Option<Color>,
-    bg: Option<Color>,
-    bold: Option<bool>,
-    intense: Option<bool>,
-    underline: Option<bool>,
-    italic: Option<bool>,
-    reset: Option<bool>,
-}
-
-impl Style {
-    /// 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)
-    }
-
-    /// Returns a token stream representing an expression constructing the
-    /// `ColorSpec` value corresponding to `self`.
-    fn to_tokens(&self) -> TokenStream {
-        let ident = Ident::new("color_spec", Span::mixed_site());
-        let mut method_calls = TokenStream::new();
-
-        if let Some(fg) = self.fg {
-            let fg = fg.to_tokens();
-            method_calls.extend(quote! {
-                #ident.set_fg(Some(#fg));
-            })
-        }
-        if let Some(bg) = self.bg {
-            let bg = bg.to_tokens();
-            method_calls.extend(quote! {
-                #ident.set_bg(Some(#bg));
-            })
-        }
-
-        macro_rules! attr {
-            ($field:ident, $method:ident) => {
-                if let Some(b) = self.$field {
-                    method_calls.extend(quote! {
-                        #ident.$method(#b);
-                    });
-                }
-            };
-        }
-
-        attr!(bold, set_bold);
-        attr!(italic, set_italic);
-        attr!(underline, set_underline);
-        attr!(intense, set_intense);
-
-        quote! {
-            {
-                let mut #ident = ::bunt::termcolor::ColorSpec::new();
-                #method_calls
-                #ident
-            }
-        }
-    }
-
-    /// Like `Option::or`: all style values set in `self` are kept, all unset
-    /// ones are overwritten with the values from `style_b`.
-    fn or(&self, style_b: Self) -> Self {
-        Self {
-            fg: self.fg.or(style_b.fg),
-            bg: self.bg.or(style_b.bg),
-            bold: self.bold.or(style_b.bold),
-            intense: self.intense.or(style_b.intense),
-            underline: self.underline.or(style_b.underline),
-            italic: self.italic.or(style_b.italic),
-            reset: self.reset.or(style_b.reset),
-        }
-    }
-}
diff --git a/macros/src/literal.rs b/macros/src/literal.rs
new file mode 100644
index 0000000..34daaaf
--- /dev/null
+++ b/macros/src/literal.rs
@@ -0,0 +1,194 @@
+//! 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;
+
+
+/// 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());
+
+    // 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
+    // wrong.
+    let s = lit.to_string();
+    if s.starts_with('r') {
+        // A raw string literal. We don't have to unescape anything, but we just
+        // have to strip the `r###` from the start and the `###` from the end.
+        let bytes = s.as_bytes();
+        let hash_count = bytes[1..].iter().take_while(|b| **b == b'#').count();
+
+        debug_assert!(hash_count < s.len() / 2, "bug: raw string literal has too many #");
+        debug_assert!(bytes[hash_count + 1] == b'"');
+        debug_assert!(bytes[s.len() - hash_count - 1] == b'"');
+
+        Ok(s[hash_count + 2..s.len() - hash_count - 1].into())
+    } else if s.starts_with('"') {
+        // A normal string literal. We have to unescape all escape sequences and
+        // strip the `"`. A non-raw literal *always* starts and ends with `"`.
+        debug_assert!(s.starts_with('"'));
+        debug_assert!(s.ends_with('"'));
+        let s = &s[1..s.len() - 1];
+
+        let mut out = String::with_capacity(s.len());
+
+        let mut i = 0;
+        while let Some(pos) = s[i..].find('\\') {
+            // Push the previous (that doesn't contain escapes) to the result.
+            out.push_str(&s[i..][..pos]);
+
+            // `pos` can't point to the last character, as that would escape the
+            // last `"`.
+            let escape = &s[i + pos..];
+            let (c, len) = match escape.as_bytes()[1] {
+                b'n' => (Some('\n'), 2),
+                b'r' => (Some('\r'), 2),
+                b't' => (Some('\t'), 2),
+                b'0' => (Some('\0'), 2),
+                b'\\' => (Some('\\'), 2),
+                b'x' => {
+                    let v = u8::from_str_radix(&escape[2..4], 16)
+                        .expect("bug: invalid \\x escape");
+                    (Some(v.into()), 4)
+                }
+                b'u' => {
+                    let end = escape.find('}').expect("invalid \\u escape");
+
+                    let v = u32::from_str_radix(&escape[3..end], 16)
+                        .expect("invalid \\u escape");
+                    let c = std::char::from_u32(v).expect("invalid value in \\u escape");
+                    (Some(c), end + 1)
+                }
+                b'\n' => {
+                    let whitespace_len = escape[2..]
+                        .char_indices()
+                        .find(|(_, c)| !c.is_whitespace())
+                        .map(|(i, _)| i)
+                        .unwrap_or(escape.len() - 2);
+                    (None, whitespace_len + 2)
+                }
+                _ => panic!("bug: unknown escape code :/"),
+            };
+
+            if let Some(c) = c {
+                out.push(c);
+            }
+            i += pos + len;
+        }
+
+        // Push the remaining string
+        out.push_str(&s[i..]);
+
+        Ok(out)
+    } else {
+        Err(Error {
+            msg: format!("literal is not a string literal"),
+            span: lit.span(),
+        })
+    }
+}
+
+
+#[cfg(test)]
+mod tests {
+    use proc_macro2::{TokenStream, TokenTree};
+    use super::*;
+    use super::parse_str_literal as parse;
+
+    // Helper function to parse a string as literal. Note that we do it this way
+    // to get other types of literal. `Literal::string` does not give us full
+    // control and neither does using `quote`.
+    fn lit(s: &str) -> Literal {
+        let stream: TokenStream = s.parse()
+            .expect("bug in test: string cannot be parse as tokenstream");
+        let mut it = stream.into_iter();
+        let tt = it.next().expect("bug in test: string is empty token stream");
+        assert!(it.next().is_none(), "bug in test: string results in multiple token trees");
+
+        match tt {
+            TokenTree::Literal(lit) => lit,
+            _ => panic!("bug in test: string didn't parse as literal"),
+        }
+    }
+
+    #[test]
+    fn empty() {
+        assert_eq!(parse(&Literal::string("")).unwrap(), "");
+        assert_eq!(parse(&lit(r###""""###)).unwrap(), "");
+        assert_eq!(parse(&lit(r###"r"""###)).unwrap(), "");
+    }
+
+    #[test]
+    fn raw() {
+        // Yes, I am aware these strings are hard to read...
+        assert_eq!(parse(&lit(r###"r"foo""###)).unwrap(), "foo");
+        assert_eq!(parse(&lit(r###"r"foo\nbar""###)).unwrap(), "foo\\nbar");
+        assert_eq!(parse(&lit(r###"r#"foo"#"###)).unwrap(), "foo");
+        assert_eq!(parse(&lit(r###"r#"foo\nbar"#"###)).unwrap(), "foo\\nbar");
+        assert_eq!(parse(&lit(r###"r##"foo"##"###)).unwrap(), "foo");
+        assert_eq!(parse(&lit(r###"r##"foo\nbar"##"###)).unwrap(), "foo\\nbar");
+    }
+
+    #[test]
+    fn normal() {
+        assert_eq!(parse(&lit(r###""foo""###)).unwrap(), "foo");
+        assert_eq!(parse(&lit(r###""foo\nbar""###)).unwrap(), "foo\nbar");
+        assert_eq!(parse(&lit(r###""foo\rbar""###)).unwrap(), "foo\rbar");
+        assert_eq!(parse(&lit(r###""foo\tbar""###)).unwrap(), "foo\tbar");
+        assert_eq!(parse(&lit(r###""foo\0bar""###)).unwrap(), "foo\0bar");
+        assert_eq!(parse(&lit(r###""foo\\bar""###)).unwrap(), "foo\\bar");
+
+        assert_eq!(parse(&lit(r###""foo\nbar\rbaz\tbuz""###)).unwrap(), "foo\nbar\rbaz\tbuz");
+
+        assert_eq!(parse(&lit(r###""foo\x50bar""###)).unwrap(), "foo\x50bar");
+        assert_eq!(parse(&lit(r###""foo\u{50}bar""###)).unwrap(), "foo\u{50}bar");
+        assert_eq!(parse(&lit(r###""foo\u{228}bar""###)).unwrap(), "foo\u{228}bar");
+        assert_eq!(parse(&lit(r###""foo\u{fffe}bar""###)).unwrap(), "foo\u{fffe}bar");
+        assert_eq!(parse(&lit(r###""foo\u{1F923}bar""###)).unwrap(), "foo\u{1F923}bar");
+    }
+
+    #[test]
+    fn wrong_kinds() {
+        assert!(parse(&Literal::u8_suffixed(27)).is_err());
+        assert!(parse(&Literal::u16_suffixed(27)).is_err());
+        assert!(parse(&Literal::u32_suffixed(27)).is_err());
+        assert!(parse(&Literal::u64_suffixed(27)).is_err());
+        assert!(parse(&Literal::u128_suffixed(27)).is_err());
+        assert!(parse(&Literal::usize_suffixed(27)).is_err());
+        assert!(parse(&Literal::i8_suffixed(-27)).is_err());
+        assert!(parse(&Literal::i16_suffixed(-27)).is_err());
+        assert!(parse(&Literal::i32_suffixed(-27)).is_err());
+        assert!(parse(&Literal::i64_suffixed(-27)).is_err());
+        assert!(parse(&Literal::i128_suffixed(-27)).is_err());
+        assert!(parse(&Literal::isize_suffixed(-27)).is_err());
+
+        assert!(parse(&Literal::u8_unsuffixed(27)).is_err());
+        assert!(parse(&Literal::u16_unsuffixed(27)).is_err());
+        assert!(parse(&Literal::u32_unsuffixed(27)).is_err());
+        assert!(parse(&Literal::u64_unsuffixed(27)).is_err());
+        assert!(parse(&Literal::u128_unsuffixed(27)).is_err());
+        assert!(parse(&Literal::usize_unsuffixed(27)).is_err());
+        assert!(parse(&Literal::i8_unsuffixed(-27)).is_err());
+        assert!(parse(&Literal::i16_unsuffixed(-27)).is_err());
+        assert!(parse(&Literal::i32_unsuffixed(-27)).is_err());
+        assert!(parse(&Literal::i64_unsuffixed(-27)).is_err());
+        assert!(parse(&Literal::i128_unsuffixed(-27)).is_err());
+        assert!(parse(&Literal::isize_unsuffixed(-27)).is_err());
+
+        assert!(parse(&Literal::f32_unsuffixed(3.14)).is_err());
+        assert!(parse(&Literal::f32_suffixed(3.14)).is_err());
+        assert!(parse(&Literal::f64_unsuffixed(3.14)).is_err());
+        assert!(parse(&Literal::f64_suffixed(3.14)).is_err());
+        assert!(parse(&Literal::f32_unsuffixed(-3.14)).is_err());
+        assert!(parse(&Literal::f32_suffixed(-3.14)).is_err());
+        assert!(parse(&Literal::f64_unsuffixed(-3.14)).is_err());
+        assert!(parse(&Literal::f64_suffixed(-3.14)).is_err());
+
+        assert!(parse(&Literal::character('a')).is_err());
+        assert!(parse(&Literal::byte_string(b"peter")).is_err());
+    }
+}
diff --git a/macros/src/parse.rs b/macros/src/parse.rs
new file mode 100644
index 0000000..f260158
--- /dev/null
+++ b/macros/src/parse.rs
@@ -0,0 +1,427 @@
+//! 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/src/lib.rs b/src/lib.rs
index a8b9a33..3cfe674 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -126,6 +126,9 @@
 // the macros.
 pub extern crate termcolor;
 
+// To consistently refer to the macros crate.
+#[doc(hidden)]
+pub extern crate bunt_macros;
 
 /// Writes formatted data to a `termcolor::WriteColor` target.
 ///
@@ -147,7 +150,15 @@ pub extern crate termcolor;
 /// ```
 ///
 /// See crate-level docs for more information.
-pub use bunt_macros::write;
+// pub use bunt_macros::write;
+#[macro_export]
+macro_rules! write {
+    ($target:expr, $format_str:literal $(, $arg:expr)* $(,)?) => {
+        $crate::bunt_macros::write!(
+            $target $format_str $( $arg )*
+        )
+    };
+}
 
 /// Writes formatted data with newline to a `termcolor::WriteColor` target.
 ///
@@ -162,8 +173,14 @@ pub use bunt_macros::write;
 /// ```
 ///
 /// See crate-level docs for more information.
-pub use bunt_macros::writeln;
-
+#[macro_export]
+macro_rules! writeln {
+    ($target:expr, $format_str:literal $(, $arg:expr)* $(,)?) => {
+        $crate::bunt_macros::writeln!(
+            $target $format_str $( $arg )*
+        )
+    };
+}
 /// Writes formatted data to stdout (with `ColorChoice::Auto`).
 ///
 /// This is like `write`, but always writes to
@@ -176,7 +193,15 @@ pub use bunt_macros::writeln;
 /// ```
 ///
 /// See crate-level docs for more information.
-pub use bunt_macros::print;
+#[macro_export]
+macro_rules! print {
+    ($format_str:literal $(, $arg:expr)* $(,)?) => {
+        $crate::bunt_macros::write!(
+            ($crate::termcolor::StandardStream::stdout($crate::termcolor::ColorChoice::Auto))
+            $format_str $( $arg )*
+        ).expect("failed to write to stdout in `bunt::print`")
+    };
+}
 
 /// Writes formatted data with newline to stdout (with `ColorChoice::Auto`).
 ///
@@ -187,7 +212,16 @@ pub use bunt_macros::print;
 /// ```
 ///
 /// See crate-level docs for more information.
-pub use bunt_macros::println;
+#[macro_export]
+macro_rules! println {
+    ($format_str:literal $(, $arg:expr)* $(,)?) => {
+        $crate::bunt_macros::writeln!(
+            ($crate::termcolor::StandardStream::stdout($crate::termcolor::ColorChoice::Auto))
+            $format_str $( $arg )*
+        ).expect("failed to write to stdout in `bunt::println`")
+    };
+}
+
 
 /// Parses the given style specification string and returns the corresponding
 /// `termcolor::ColorSpec` value.


Test patch: