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 { + // 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, +} + +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, + + /// Information about argument that are referenced. + args: Vec, + }, + + /// 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, + pub(crate) named: HashMap, +} + +#[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, + pub(crate) bg: Option, + pub(crate) bold: Option, + pub(crate) intense: Option, + pub(crate) underline: Option, + pub(crate) italic: Option, + pub(crate) reset: Option, +} + +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::(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::(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::(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::(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::(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 { - // 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 { - let target = input.parse()?; - input.parse::()?; - 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 { - 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 { - 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, - - /// Information about argument that are referenced. - args: Vec, - }, - - /// 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 { - // 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::() { - ArgRefKind::Position(pos) - } else { - syn::parse_str::(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, -} - -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 { - /// 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::()?; - 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, - named: HashMap, -} - -impl Parse for FormatArgs { - fn parse(input: ParseStream) -> Result { - 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::()?; - - 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::()?; - } - - 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 { - 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, - bg: Option, - bold: Option, - intense: Option, - underline: Option, - italic: Option, - reset: Option, -} - -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 { - 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::, _>>()?; - - 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 { + // 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(tokens: TokenStream, f: F) -> Result +where + F: FnOnce(&mut TokenIterator) -> Result, +{ + 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) -> 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 { + 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 { + let (tokens, span) = expect_helper_group(it.next())?; + Ok(Self { tokens, span }) + } +} + +impl FormatStr { + pub(crate) fn parse(it: &mut TokenIterator) -> Result { + /// 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 { + // 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::() { + 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 { + 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 { + 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::, _>>()?; + + 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 { + /// 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, tt1: &Option) -> Option { + 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.