color-print-proc-macro-0.3.7/.cargo_vcs_info.json0000644000000001640000000000100153540ustar { "git": { "sha1": "e42619565a566d12a0da4eb930940e918bff4db1" }, "path_in_vcs": "color-print-proc-macro" }color-print-proc-macro-0.3.7/Cargo.toml0000644000000021570000000000100133560ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "color-print-proc-macro" version = "0.3.7" authors = ["Johann David "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Implementation for the package color-print" readme = "README.md" license = "MIT OR Apache-2.0" repository = "https://gitlab.com/dajoha/color-print" [lib] name = "color_print_proc_macro" path = "src/lib.rs" proc-macro = true [dependencies.nom] version = "7.1" [dependencies.proc-macro2] version = "1.0" [dependencies.quote] version = "1.0" [dependencies.syn] version = "2.0" features = ["full"] [features] terminfo = [] color-print-proc-macro-0.3.7/Cargo.toml.orig000064400000000000000000000006701046102023000170350ustar 00000000000000[package] name = "color-print-proc-macro" version = "0.3.7" edition = "2018" authors = ["Johann David "] license = "MIT OR Apache-2.0" description = "Implementation for the package color-print" repository = "https://gitlab.com/dajoha/color-print" [lib] proc-macro = true [features] terminfo = [] [dependencies] syn = { version = "2.0", features = ["full"] } quote = "1.0" proc-macro2 = "1.0" nom = "7.1" color-print-proc-macro-0.3.7/LICENSE-APACHE000064400000000000000000000227731046102023000161020ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS color-print-proc-macro-0.3.7/LICENSE-MIT000064400000000000000000000017771046102023000156130ustar 00000000000000Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. color-print-proc-macro-0.3.7/README.md000064400000000000000000000002211046102023000154150ustar 00000000000000This internal library provides the procedural macros needed by the crate [`color-print`]. [`color-print`]: https://crates.io/crates/color-print color-print-proc-macro-0.3.7/src/ansi.rs000064400000000000000000000055631046102023000162430ustar 00000000000000//! This module is only used when the feature `terminfo` is not activated. use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens}; use crate::color_context::Context; use crate::error::{SpanError, Error}; use crate::format_args::{ parse_args, get_format_string, get_args_and_format_string, parse_format_string, Node }; /// Common code shared between the public macros, ANSI implementation. pub fn get_format_args(input: TokenStream) -> Result { let (format_string_token, args) = get_args_and_format_string(input)?; let format_string = format_string_token.value(); // Split the format string into a list of nodes; each node is either a string literal (text), a // placeholder for a `format!`-like macro, or a color code: let format_nodes = parse_format_string(&format_string, &format_string_token)?; let final_format_string = get_format_string_from_nodes(format_nodes)?; // Group all the final arguments into a single iterator: let args = args.iter() .map(|arg| arg.to_token_stream()) .skip(1); // Skip the original format string let final_args = std::iter::once(final_format_string).chain(args); Ok(quote! { #(#final_args),* }) } /// Transforms a string literal by parsing its color tags. pub fn get_cstr(input: TokenStream) -> Result { let args = parse_args(input)?; let format_string_token = get_format_string(args.first())?; let format_string = format_string_token.value(); if args.len() > 1 { return Err(SpanError::new(Error::TooManyArgs, None)); } // Split the format string into a list of nodes; each node is either a string literal (text), // or a color code; `format!`-like placeholders will be parsed indenpendently, but as they are // put back unchanged into the format string, it's not a problem: let format_nodes = parse_format_string(&format_string, &format_string_token)?; get_format_string_from_nodes(format_nodes) } /// Generates a new format string with the color tags replaced by the right ANSI codes. fn get_format_string_from_nodes(nodes: Vec) -> Result { // The final, modified format string which will be given to the `format!`-like macro: let mut format_string = String::new(); // Stores which colors and attributes are set while processing the format string: let mut color_context = Context::new(); // Generate the final format string: for node in nodes { match node { Node::Text(s) | Node::Placeholder(s) => { format_string.push_str(s); } Node::ColorTagGroup(tag_group) => { let ansi_string = color_context.ansi_apply_tags(tag_group)?; format_string.push_str(&ansi_string); } } } Ok(quote! { #format_string }) } color-print-proc-macro-0.3.7/src/ansi_constants.rs000064400000000000000000000026661046102023000203400ustar 00000000000000//! ANSI constants. // pub const RESET: u8 = 0; pub const BOLD: u8 = 1; pub const DIM: u8 = 2; pub const ITALIC: u8 = 3; pub const UNDERLINE: u8 = 4; pub const BLINK: u8 = 5; pub const REVERSE: u8 = 7; pub const CONCEAL: u8 = 8; pub const STRIKE: u8 = 9; pub const NO_BOLD: u8 = 22; pub const NO_ITALIC: u8 = 23; pub const NO_UNDERLINE: u8 = 24; pub const NO_BLINK: u8 = 25; pub const NO_REVERSE: u8 = 27; pub const NO_CONCEAL: u8 = 28; pub const NO_STRIKE: u8 = 29; pub const SET_FOREGROUND_BASE: u8 = 30; pub const SET_FOREGROUND: u8 = 38; pub const DEFAULT_FOREGROUND: u8 = 39; pub const SET_BACKGROUND_BASE: u8 = 40; pub const SET_BACKGROUND: u8 = 48; pub const DEFAULT_BACKGROUND: u8 = 49; pub const SET_BRIGHT_FOREGROUND_BASE: u8 = 90; pub const SET_BRIGHT_BACKGROUND_BASE: u8 = 100; /// Generate an SGR ANSI sequence. pub fn generate_ansi_code(params: &[u8]) -> String { let mut ansi_code = String::from("\u{1b}["); let mut first = true; for param in params { if first { first = false; } else { ansi_code.push(';'); } ansi_code.push_str(&format!("{}", param)); } ansi_code.push('m'); ansi_code } #[cfg(test)] mod tests { use super::*; #[test] fn ansi_code() { assert_eq!(generate_ansi_code(&[0]), "\u{1b}[0m"); assert_eq!(generate_ansi_code(&[31]), "\u{1b}[31m"); assert_eq!(generate_ansi_code(&[38, 5, 1]), "\u{1b}[38;5;1m"); } } color-print-proc-macro-0.3.7/src/color_context.rs000064400000000000000000000664651046102023000202030ustar 00000000000000//! This module permits to determine which ANSI sequences have to be added at a given position in //! the format string, by saving the current tags in a "context". When a new tag is encountered, a //! diff between the old state and the new state is performed to determine the right ANSI sequences //! to add. use std::convert::TryFrom; use proc_macro2::Span; use crate::error::{Error, SpanError}; /// Stores all the current open tags encountered in the format string. #[derive(Debug, PartialEq, Default)] pub struct Context<'a>(Vec>); impl<'a> Context<'a> { pub fn new() -> Self { Self::default() } /// Applies a group of tags to the current context, and returns a list of the terminfo /// constants (available in the `color-print` package) to be added as named arguments at the /// end of the format arguments. /// /// For each given tag: /// - if the tag is an open tag, push it into the context; /// - if it's a valid close tag, pop the last open tag. #[cfg(feature = "terminfo")] pub fn terminfo_apply_tags( &mut self, tag_group: Vec>, ) -> Result, SpanError> { let state_diff = self.apply_tags_and_get_diff(tag_group)?; Ok(state_diff.terminfo_token_streams()) } /// Applies a group of tags to the current context, and returns the ANSI sequences to be /// added into the format string. /// /// For each given tag: /// - if the tag is an open tag, push it into the context; /// - if it's a valid close tag, pop the last open tag. #[cfg(not(feature = "terminfo"))] pub fn ansi_apply_tags(&mut self, tag_group: Vec>) -> Result { let state_diff = self.apply_tags_and_get_diff(tag_group)?; Ok(state_diff.ansi_string()) } /// Applies a group of tags to the current context, with no return on success. Used by the /// macro `untagged!()`. /// /// For each given tag: /// - if the tag is an open tag, push it into the context; /// - if it's a valid close tag, pop the last open tag. pub fn apply_tags(&mut self, tag_group: Vec>) -> Result<(), SpanError> { self.apply_tags_and_get_diff(tag_group).map(|_| ()) } /// Returns the actual color/style state, which is the result of the changes made by each tag /// sequentially. pub fn state(&self) -> State { let mut state = State::default(); for tag in &self.0 { if let Some(ref color) = tag.change_set.foreground { state.foreground = ExtColor::Color(color.clone()); } if let Some(ref color) = tag.change_set.background { state.background = ExtColor::Color(color.clone()); } state.bold |= tag.change_set.bold; state.dim |= tag.change_set.dim; state.underline |= tag.change_set.underline; state.italics |= tag.change_set.italics; state.blink |= tag.change_set.blink; state.strike |= tag.change_set.strike; state.reverse |= tag.change_set.reverse; state.conceal |= tag.change_set.conceal; } state } #[allow(rustdoc::broken_intra_doc_links)] /// Common code betwwen [Self::terminfo_apply_tag()] and [Self::ansi_apply_tag()]. fn apply_tags_and_get_diff(&mut self, tags: Vec>) -> Result { let old_state = self.state(); for tag in tags { if tag.is_close { let last_tag = self.0.last() .ok_or_else(|| SpanError::new(Error::NoTagToClose, tag.span))?; // If the tag is "void" (it is a "" tag), we don't need to check if the change // sets are matching: if !tag.change_set.is_void() && last_tag.change_set != tag.change_set { let (last_src, src) = ( // We can unwrap the last tag source, because we know that all the tags // stored inside the context are *open tags*, and open tag are always taken // from the source input: last_tag.source.unwrap(), // We can unwrap the source of the tag currently being processed, because // we just checked above that the tag is not void, and non-void tags are // always taken from the source input: tag.source.unwrap(), ); return Err(SpanError::new( Error::MismatchCloseTag(last_src.to_owned(), src.to_owned()), tag.span, )); } self.0.pop().unwrap(); } else { self.0.push(tag); } } let new_state = self.state(); Ok(StateDiff::from_diff(&old_state, &new_state)) } } /// Describes the state of each color and style attributes at a given position in the format /// string. Two states can be compared together by creating a [`StateDiff`] instance. #[derive(Debug, PartialEq, Default)] pub struct State { foreground: ExtColor, background: ExtColor, bold: bool, dim: bool, underline: bool, italics: bool, blink: bool, strike: bool, reverse: bool, conceal: bool, } /// The result of the comparison between two [`State`]s. /// /// Each field is an [`Action`], which indicates if the given value has to be changed or left /// unchanged in order to reach the new state. #[derive(Debug)] pub struct StateDiff { foreground: Action, background: Action, bold: Action, dim: Action, underline: Action, italics: Action, blink: Action, #[cfg(not(feature = "terminfo"))] strike: Action, reverse: Action, #[cfg(not(feature = "terminfo"))] conceal: Action, } impl StateDiff { /// Creates a new [`StateDiff`] by comparing two [`State`]s. pub fn from_diff(old: &State, new: &State) -> Self { StateDiff { foreground: Action::from_diff(Some(old.foreground.clone()), Some(new.foreground.clone())), background: Action::from_diff(Some(old.background.clone()), Some(new.background.clone())), bold: Action::from_diff(Some(old.bold), Some(new.bold)), dim: Action::from_diff(Some(old.dim), Some(new.dim)), underline: Action::from_diff(Some(old.underline), Some(new.underline)), italics: Action::from_diff(Some(old.italics), Some(new.italics)), blink: Action::from_diff(Some(old.blink), Some(new.blink)), #[cfg(not(feature = "terminfo"))] strike: Action::from_diff(Some(old.strike), Some(new.strike)), reverse: Action::from_diff(Some(old.reverse), Some(new.reverse)), #[cfg(not(feature = "terminfo"))] conceal: Action::from_diff(Some(old.conceal), Some(new.conceal)), } } /// Returns the list of terminfo constants (available in the `color-print` package) which have /// to be used in order to reach the new state. #[cfg(feature = "terminfo")] pub fn terminfo_token_streams(&self) -> Vec { let mut constants = vec![]; macro_rules! push_constant { ($s:expr) => {{ constants.push($s.to_owned()); }}; } let have_to_reset = or!( matches!(self.foreground, Action::Change(ExtColor::Normal)), matches!(self.background, Action::Change(ExtColor::Normal)), matches!(self.bold, Action::Change(false)), matches!(self.dim, Action::Change(false)), matches!(self.blink, Action::Change(false)), matches!(self.reverse, Action::Change(false)), ); if have_to_reset { push_constant!("CLEAR"); if let Some(ExtColor::Color(Color::Color16(color))) = self.foreground.actual_value() { push_constant!(color.terminfo_constant(true)); } if let Some(ExtColor::Color(Color::Color16(color))) = self.background.actual_value() { push_constant!(color.terminfo_constant(false)); } if matches!(self.bold.actual_value(), Some(true)) { push_constant!("BOLD"); } if matches!(self.dim.actual_value(), Some(true)) { push_constant!("DIM"); } if matches!(self.blink.actual_value(), Some(true)) { push_constant!("BLINK"); } if matches!(self.underline.actual_value(), Some(true)) { push_constant!("UNDERLINE"); } if matches!(self.italics.actual_value(), Some(true)) { push_constant!("ITALICS"); } if matches!(self.reverse.actual_value(), Some(true)) { push_constant!("REVERSE"); } } else { if let Action::Change(ExtColor::Color(Color::Color16(ref color))) = self.foreground { push_constant!(color.terminfo_constant(true)); } if let Action::Change(ExtColor::Color(Color::Color16(ref color))) = self.background { push_constant!(color.terminfo_constant(false)); } if let Action::Change(true) = self.bold { push_constant!("BOLD"); } if let Action::Change(true) = self.dim { push_constant!("DIM"); } if let Action::Change(true) = self.blink { push_constant!("BLINK"); } if let Action::Change(true) = self.reverse { push_constant!("REVERSE"); } if let Action::Change(underline) = self.underline { let constant = if underline { "UNDERLINE" } else { "NO_UNDERLINE" }; push_constant!(constant); } if let Action::Change(italics) = self.italics { let constant = if italics { "ITALICS" } else { "NO_ITALICS" }; push_constant!(constant); } } constants } /// Returns the ANSI sequence(s) which has to added to the format string in order to reach the /// new state. #[cfg(not(feature = "terminfo"))] pub fn ansi_string(&self) -> String { use crate::ansi_constants::*; let mut output = String::new(); macro_rules! push_code { ($($codes:expr),*) => { output.push_str(&generate_ansi_code(&[$($codes),*])) }; } if let Action::Change(ref ext_color) = self.foreground { match ext_color { ExtColor::Normal => push_code!(DEFAULT_FOREGROUND), ExtColor::Color(Color::Color16(color)) => match color.intensity { Intensity::Normal => { push_code!(SET_FOREGROUND_BASE + color.base_color.index()) } Intensity::Bright => { push_code!(SET_BRIGHT_FOREGROUND_BASE + color.base_color.index()) } }, ExtColor::Color(Color::Color256(color)) => { push_code!(SET_FOREGROUND, 5, color.0); }, ExtColor::Color(Color::ColorRgb(color)) => { push_code!(SET_FOREGROUND, 2, color.r, color.g, color.b); }, } } if let Action::Change(ref ext_color) = self.background { match ext_color { ExtColor::Normal => push_code!(DEFAULT_BACKGROUND), ExtColor::Color(Color::Color16(color)) => match color.intensity { Intensity::Normal => { push_code!(SET_BACKGROUND_BASE + color.base_color.index()) } Intensity::Bright => { push_code!(SET_BRIGHT_BACKGROUND_BASE + color.base_color.index()) } }, ExtColor::Color(Color::Color256(color)) => { push_code!(SET_BACKGROUND, 5, color.0); }, ExtColor::Color(Color::ColorRgb(color)) => { push_code!(SET_BACKGROUND, 2, color.r, color.g, color.b); }, } } macro_rules! handle_attr { ($attr:expr, $true_val:expr, $false_val:expr) => { match $attr { Action::Change(true) => push_code!($true_val), Action::Change(false) => push_code!($false_val), _ => (), } }; } handle_attr!(self.bold, BOLD, NO_BOLD); handle_attr!(self.dim, DIM, NO_BOLD); handle_attr!(self.underline, UNDERLINE, NO_UNDERLINE); handle_attr!(self.italics, ITALIC, NO_ITALIC); handle_attr!(self.blink, BLINK, NO_BLINK); handle_attr!(self.strike, STRIKE, NO_STRIKE); handle_attr!(self.reverse, REVERSE, NO_REVERSE); handle_attr!(self.conceal, CONCEAL, NO_CONCEAL); output } } /// The action to be performed on a given color/style attribute in order to reach a new state. #[derive(Debug, PartialEq)] pub enum Action { /// Nothing has to be done, because this value was never modified. None, /// This attribute has to be kept the same. /// With the terminfo implementation, it's not possible to reset each style/color /// independently, so we have to keep track of the values, even with the `Keep` variant. Keep(T), /// This attribute value has to be changed. Change(T), } #[cfg(feature = "terminfo")] impl Action { pub fn actual_value(&self) -> Option<&T> { match self { Action::Keep(val) | Action::Change(val) => Some(val), Action::None => None, } } } impl Action where T: PartialEq, { /// Creates a new [`Action`]. pub fn from_diff(old: Option, new: Option) -> Self { let eq = old == new; match (old, new, eq) { (Some(old_val), Some(_), true) | (Some(old_val), None, _) => Action::Keep(old_val), (_, Some(new_val), _) => Action::Change(new_val), _ => Action::None, } } } /// A parsed color/style tag. #[derive(Debug, Default)] pub struct ColorTag<'a> { /// Source of the tag in the format string. pub source: Option<&'a str>, /// Span of the tag in the format string. pub span: Option, /// Is it a close tag like ``. pub is_close: bool, /// The changes that are implied by this tag. pub change_set: ChangeSet, } impl<'a> PartialEq for ColorTag<'a> { fn eq(&self, other: &ColorTag<'a>) -> bool { and!( self.source == other.source, self.is_close == other.is_close, self.change_set == other.change_set, ) } } impl<'a> ColorTag<'a> { /// Creates a new close tag; only used in order to auto-close unclosed tags at the end of the /// format string. pub fn new_close() -> Self { ColorTag { source: None, span: None, is_close: true, change_set: ChangeSet::default(), } } /// Sets the span of the tag. pub fn set_span(&mut self, span: Span) { self.span = Some(span); } } /// The changes that are implied by a tag. #[derive(Debug, PartialEq, Default)] pub struct ChangeSet { /// If it is `Some`, then the foreground color has to be changed. pub foreground: Option, /// If it is `Some`, then the background color has to be changed. pub background: Option, /// If it is `true`, then the bold attribute has to be set (or unset for a close tag). pub bold: bool, /// If it is `true`, then the dim attribute has to be set (or unset for a close tag). pub dim: bool, /// If it is `true`, then the underline attribute has to be set (or unset for a close tag). pub underline: bool, /// If it is `true`, then the italics attribute has to be set (or unset for a close tag). pub italics: bool, /// If it is `true`, then the blink attribute has to be set (or unset for a close tag). pub blink: bool, /// If it is `true`, then the strike attribute has to be set (or unset for a close tag). pub strike: bool, /// If it is `true`, then the reverse attribute has to be set (or unset for a close tag). pub reverse: bool, /// If it is `true`, then the conceal attribute has to be set (or unset for a close tag). pub conceal: bool, } impl ChangeSet { /// Checks if there is nothing to change (used to detect the `` tag). pub fn is_void(&self) -> bool { and!( self.foreground.is_none(), self.background.is_none(), !self.bold, !self.dim, !self.underline, !self.italics, !self.blink, !self.strike, !self.reverse, !self.conceal, ) } } impl From<&[Change]> for ChangeSet { fn from(changes: &[Change]) -> ChangeSet { let mut change_set = ChangeSet::default(); for change in changes { match change { Change::Foreground(color) => change_set.foreground = Some(color.clone()), Change::Background(color) => change_set.background = Some(color.clone()), Change::Bold => change_set.bold = true, Change::Dim => change_set.dim = true, Change::Underline => change_set.underline = true, Change::Italics => change_set.italics = true, Change::Blink => change_set.blink = true, Change::Strike => change_set.strike = true, Change::Reverse => change_set.reverse = true, Change::Conceal => change_set.conceal = true, } } change_set } } /// A single change to be done inside a tag. Tags with multiple keywords like `` will /// have multiple [`Change`]s. #[derive(Debug, PartialEq, Clone)] pub enum Change { Foreground(Color), Background(Color), Bold, Dim, Underline, Italics, Blink, Strike, Reverse, Conceal, } impl TryFrom<&str> for Change { type Error = (); /// Tries to convert a keyword like `red`, `bold` into a [`Change`] instance. #[rustfmt::skip] fn try_from(input: &str) -> Result { macro_rules! color16 { ($kind:ident $intensity:ident $base_color:ident) => { Change::$kind(Color::Color16(Color16::new( BaseColor::$base_color, Intensity::$intensity, ))) }; } let change = match input { "s" | "strong" | "bold" | "em" => Change::Bold, "dim" => Change::Dim, "u" | "underline" => Change::Underline, "i" | "italic" | "italics" => Change::Italics, "blink" => Change::Blink, "strike" => Change::Strike, "reverse" | "rev" => Change::Reverse, "conceal" | "hide" => Change::Conceal, "k" | "black" => color16!(Foreground Normal Black), "r" | "red" => color16!(Foreground Normal Red), "g" | "green" => color16!(Foreground Normal Green), "y" | "yellow" => color16!(Foreground Normal Yellow), "b" | "blue" => color16!(Foreground Normal Blue), "m" | "magenta" => color16!(Foreground Normal Magenta), "c" | "cyan" => color16!(Foreground Normal Cyan), "w" | "white" => color16!(Foreground Normal White), "k!" | "black!" | "bright-black" => color16!(Foreground Bright Black), "r!" | "red!" | "bright-red" => color16!(Foreground Bright Red), "g!" | "green!" | "bright-green" => color16!(Foreground Bright Green), "y!" | "yellow!" | "bright-yellow" => color16!(Foreground Bright Yellow), "b!" | "blue!" | "bright-blue" => color16!(Foreground Bright Blue), "m!" | "magenta!" | "bright-magenta" => color16!(Foreground Bright Magenta), "c!" | "cyan!" | "bright-cyan" => color16!(Foreground Bright Cyan), "w!" | "white!" | "bright-white" => color16!(Foreground Bright White), "K" | "bg-black" => color16!(Background Normal Black), "R" | "bg-red" => color16!(Background Normal Red), "G" | "bg-green" => color16!(Background Normal Green), "Y" | "bg-yellow" => color16!(Background Normal Yellow), "B" | "bg-blue" => color16!(Background Normal Blue), "M" | "bg-magenta" => color16!(Background Normal Magenta), "C" | "bg-cyan" => color16!(Background Normal Cyan), "W" | "bg-white" => color16!(Background Normal White), "K!" | "bg-black!" | "bg-bright-black" => color16!(Background Bright Black), "R!" | "bg-red!" | "bg-bright-red" => color16!(Background Bright Red), "G!" | "bg-green!" | "bg-bright-green" => color16!(Background Bright Green), "Y!" | "bg-yellow!" | "bg-bright-yellow" => color16!(Background Bright Yellow), "B!" | "bg-blue!" | "bg-bright-blue" => color16!(Background Bright Blue), "M!" | "bg-magenta!" | "bg-bright-magenta" => color16!(Background Bright Magenta), "C!" | "bg-cyan!" | "bg-bright-cyan" => color16!(Background Bright Cyan), "W!" | "bg-white!" | "bg-bright-white" => color16!(Background Bright White), _ => return Err(()), }; Ok(change) } } /// Which "kind" of color has to be changed. #[derive(Debug, PartialEq, Clone)] pub enum ColorKind { Background, Foreground, } impl ColorKind { pub fn to_change(&self, color: Color) -> Change { match self { Self::Foreground => Change::Foreground(color), Self::Background => Change::Background(color), } } } /// An "extended" color, which can be either a real color or the "normal", default color. #[derive(Debug, PartialEq, Clone)] pub enum ExtColor { Normal, Color(Color), } impl Default for ExtColor { fn default() -> Self { Self::Normal } } #[derive(Debug, PartialEq, Clone)] #[allow(clippy::enum_variant_names)] pub enum Color { Color16(Color16), Color256(Color256), ColorRgb(ColorRgb), } /// A terminal color. #[derive(Debug, PartialEq, Clone)] pub struct Color16 { base_color: BaseColor, intensity: Intensity, } impl Color16 { pub fn new(base_color: BaseColor, intensity: Intensity) -> Self { Self { base_color, intensity } } /// Converts a color to a terminfo constant name (available in the `color-print` package). #[cfg(feature = "terminfo")] pub fn terminfo_constant(&self, is_foreground: bool) -> String { let mut constant = if is_foreground { String::new() } else { "BG_".to_string() }; if matches!(self.intensity, Intensity::Bright) { constant.push_str("BRIGHT_"); } constant.push_str(self.base_color.uppercase_str()); constant } } /// The intensity of a terminal color. #[derive(Debug, PartialEq, Copy, Clone)] pub enum Intensity { Normal, Bright, } impl Intensity { pub fn new(is_bright: bool) -> Self { if is_bright { Self::Bright } else { Self::Normal } } } /// A "base" terminal color, which has to be completed with an [`Intensity`] in order to describe a /// whole terminal color. #[derive(Debug, PartialEq, Copy, Clone)] pub enum BaseColor { Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, } impl BaseColor { /// Return the index of a color, in the same ordering as the ANSI color sequences. #[cfg(not(feature = "terminfo"))] pub fn index(&self) -> u8 { match self { Self::Black => 0, Self::Red => 1, Self::Green => 2, Self::Yellow => 3, Self::Blue => 4, Self::Magenta => 5, Self::Cyan => 6, Self::White => 7, } } /// Used to generate terminfo constants, see [`Color16::terminfo_constant()`]. #[cfg(feature = "terminfo")] pub fn uppercase_str(&self) -> &'static str { match self { Self::Black => "BLACK", Self::Red => "RED", Self::Green => "GREEN", Self::Yellow => "YELLOW", Self::Blue => "BLUE", Self::Magenta => "MAGENTA", Self::Cyan => "CYAN", Self::White => "WHITE", } } } /// A color in the 256-color palette. #[derive(Debug, PartialEq, Clone)] pub struct Color256(pub u8); /// An RGB color. #[derive(Debug, PartialEq, Clone)] pub struct ColorRgb { pub r: u8, pub g: u8, pub b: u8, } #[cfg(test)] mod tests { #[cfg(feature = "terminfo")] use super::*; #[cfg(feature = "terminfo")] use crate::parse::color_tag; #[test] #[cfg(feature = "terminfo")] fn terminfo_apply_tag_to_context() { let mut context = Context::new(); macro_rules! apply_tag { ($s:expr) => { context .terminfo_apply_tags(vec![color_tag($s).unwrap().1]) .unwrap() }; } let constants = apply_tag!(""); assert_eq!(constants, ["RED"]); let constants = apply_tag!(""); assert_eq!(constants, ["CLEAR"]); let constants = apply_tag!(""); assert_eq!(constants, ["RED"]); let constants = apply_tag!(""); assert_eq!(constants, ["BOLD"]); let constants = apply_tag!(""); assert_eq!(constants, ["CLEAR", "RED"]); let constants = apply_tag!(""); assert_eq!(constants, ["CLEAR"]); } #[test] #[cfg(feature = "terminfo")] fn terminfo_apply_tag_to_context_2() { let mut context = Context::new(); macro_rules! apply_tag { ($s:expr) => { context .terminfo_apply_tags(vec![color_tag($s).unwrap().1]) .unwrap() }; } let constants = apply_tag!(""); assert_eq!(constants, ["RED"]); let constants = apply_tag!(""); assert_eq!(constants, ["BG_YELLOW"]); let constants = apply_tag!(""); assert_eq!(constants, ["BOLD"]); let constants = apply_tag!(""); assert_eq!(constants, ["UNDERLINE"]); let constants = apply_tag!(""); assert_eq!(constants, ["NO_UNDERLINE"]); let constants = apply_tag!(""); assert_eq!(constants, ["CLEAR", "RED", "BG_YELLOW"]); } #[test] #[cfg(feature = "terminfo")] fn terminfo_apply_tag_to_context_3() { let mut context = Context::new(); macro_rules! apply_tag { ($s:expr) => { context.terminfo_apply_tags(vec![color_tag($s).unwrap().1]) }; } let res = apply_tag!(""); assert_eq!(res, Err(SpanError::new(Error::NoTagToClose, None))); apply_tag!("").unwrap(); let res = apply_tag!(""); assert_eq!( res, Err(SpanError::new( Error::MismatchCloseTag("".to_owned(), "".to_owned()), None )) ); } } color-print-proc-macro-0.3.7/src/error.rs000064400000000000000000000061421046102023000164340ustar 00000000000000use std::fmt; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::ToTokens; /// An error with an optional span. Most errors will have a span, the only exception is on a /// [`Error::Parse`], which can occur in the [`get_args_and_format_string()`] function. /// /// [`get_args_and_format_string()`]: crate::format_args::get_args_and_format_string() #[derive(Debug, Clone)] pub struct SpanError { pub err: Error, pub span: Option, } impl SpanError { /// Creates a new `SpanError`. pub fn new(err: Error, span: Option) -> Self { Self { err, span } } } /// Manual implementation because [`Span`] is not [`PartialEq`] and can be ignored when comparing. impl PartialEq for SpanError { fn eq(&self, other: &SpanError) -> bool { self.err == other.err } } impl From for SpanError { fn from(err: Error) -> SpanError { SpanError { err, span: None } } } impl ToTokens for SpanError { fn to_tokens(&self, tokens: &mut TokenStream2) { let span = self.span.unwrap_or_else(Span::call_site); let token_stream_err = syn::Error::new(span, self.err.clone()).to_compile_error(); token_stream_err.to_tokens(tokens); } } /// All possible errors which can occur when calling one of the public macros. #[derive(Debug, PartialEq, Clone)] pub enum Error { /// Error during the initial parsing of the macro arguments. Parse(String), /// The first macro argument is not a string literal. MustBeStringLiteral, /// Unable to parse a tag. UnableToParseTag(String), /// An error occured while parsing a color tag. ParseTag(String), /// A "{" character has not been closed in the format string. UnclosedPlaceholder, /// A "<" character has not been closed in the format string. UnclosedTag, /// Trying to close a previous tag, while there are no open tag. NoTagToClose, /// Trying to close a previous tag which does not match, like `...`. MismatchCloseTag(String, String), /// Only one argument is allowed for the `cstr!()` and `untagged!()` macros. #[cfg(not(feature = "terminfo"))] TooManyArgs, /// Only one argument is allowed for the '`untagged!()` macro. #[cfg(feature = "terminfo")] TooManyArgs, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let msg = match self { Self::Parse(msg) => msg.clone(), Self::MustBeStringLiteral => "Format string must be a string literal".to_owned(), Self::UnableToParseTag(tag) => format!("Unable to parse the tag {}", tag), Self::ParseTag(detail) => detail.clone(), Self::UnclosedPlaceholder => "Unclosed placeholder".to_owned(), Self::UnclosedTag => "Unclosed color tag".to_owned(), Self::NoTagToClose => "No color tag to close".to_owned(), Self::MismatchCloseTag(tag1, tag2) => { format!("Mismatch close tag between {} and {}", tag1, tag2) } Self::TooManyArgs => "Too many arguments".to_owned(), }; write!(f, "{}", msg) } } color-print-proc-macro-0.3.7/src/format_args/format_arg.rs000064400000000000000000000022401046102023000217230ustar 00000000000000//! An argument in a `format!`-like macro, parsable and transformable to tokens. use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; use syn::{ parse::{Parse, ParseStream, Result}, token, Expr, Ident, Token, }; /// An argument in a `format!`-like macro (excluding the first argument aka the format string). pub struct FormatArg { /// The argument name in the case of a named argument, e.g. `foo` inside `foo = 1 + 1`. pub arg_name: Option<(Ident, token::Eq)>, /// The real argument to be formatted by the macro. pub expr: Expr, } impl Parse for FormatArg { fn parse(input: ParseStream) -> Result { let arg_name: Option<(Ident, token::Eq)> = if input.peek2(Token![=]) { Some((input.parse()?, input.parse()?)) } else { None }; let expr: Expr = input.parse()?; Ok(FormatArg { arg_name, expr }) } } impl ToTokens for FormatArg { fn to_tokens(&self, tokens: &mut TokenStream2) { if let Some((arg_name, eq)) = &self.arg_name { arg_name.to_tokens(tokens); eq.to_tokens(tokens); } self.expr.to_tokens(tokens); } } color-print-proc-macro-0.3.7/src/format_args/mod.rs000064400000000000000000000166311046102023000203720ustar 00000000000000//! Parses the `format!`-like arguments and the format string. mod format_arg; use proc_macro::TokenStream; use syn::spanned::Spanned; use syn::{ self, parse::Parser, punctuated::Punctuated, token::Comma, Expr, ExprLit, Lit, LitStr, Token, }; use crate::color_context::ColorTag; use crate::error::{Error, SpanError}; use crate::parse; use crate::util::{self, inner_span}; use format_arg::FormatArg; /// Retrieves the original format string and arguments given to the public macros. pub fn get_args_and_format_string( input: TokenStream, ) -> Result<(LitStr, Punctuated), SpanError> { let args = parse_args(input)?; let format_string = get_format_string(args.first())?; Ok((format_string, args)) } /// Parses the arguments of a `format!`-like macro. pub fn parse_args(input: TokenStream) -> Result, SpanError> { let parser = Punctuated::::parse_terminated; parser .parse(input) .map_err(|e| Error::Parse(e.to_string()).into()) } /// Gets the format string. pub fn get_format_string(arg: Option<&FormatArg>) -> Result { match arg { Some(FormatArg { expr: Expr::Lit(ExprLit { lit: Lit::Str(s), .. }), .. }) => Ok(s.to_owned()), Some(bad_arg) => { Err(SpanError::new(Error::MustBeStringLiteral, Some(bad_arg.span()),)) } None => Ok(util::literal_string("")) } } /// A node inside a format string. The two variants `Text` and `Placeholder` represent the usual /// kind of nodes that can be found in `format!`-like macros. /// /// E.g., the format string `"Hello, {:?}, happy day"` will have 3 nodes: /// - `Text("Hello, ")`, /// - `Placeholder("{:?}")`, /// - `Text(", happy day")`. /// /// The third kind of node: `Color(&str)`, represents a color code to apply. /// /// E.g., the format string `"This is a {} idea"` will have 7 nodes: /// - `Text("This is a ")` /// - `Color("blue")` /// - `Placeholder("{}")` /// - `Color("clear")` /// - `Text(" idea")` #[derive(Debug)] pub enum Node<'a> { Text(&'a str), Placeholder(&'a str), ColorTagGroup(Vec>), } /// Parses a format string which may contain usual format placeholders (`{...}`) as well as color /// codes like `""`, `""`. pub fn parse_format_string<'a>( input: &'a str, lit_str: &LitStr, ) -> Result>, SpanError> { /// Representation of the parsing context. Each variant's argument is the start offset of the /// given parse context. enum Context { /// The char actually parsed is a textual character: Text(usize), /// The char actually parsed is part of a `format!`-like placeholder: Placeholder(usize), /// The char actually parsed is part of a color tag, like ``: Color(usize), } macro_rules! span { ($inside:expr) => { inner_span(input, lit_str, $inside) }; } macro_rules! err { ([$inside:expr] $($e:tt)*) => { SpanError::new($($e)*, Some(span!($inside))) }; ($($e:tt)*) => { SpanError::new($($e)*, Some(lit_str.span())) }; } let mut context = Context::Text(0); let mut nodes = vec![]; let mut close_angle_bracket_idx: Option = None; let mut nb_open_tags: isize = 0; for (i, c) in input.char_indices() { match context { Context::Text(text_start) => { let mut push_text = false; if c == '{' { // The start of a placeholder: context = Context::Placeholder(i); push_text = true; } else if c == '<' { // The start of a color code: context = Context::Color(i); push_text = true; } else if c == '>' { // Double close angle brackets ">>": if let Some(idx) = close_angle_bracket_idx { if i == idx + 1 { context = Context::Text(i + 1); push_text = true; } close_angle_bracket_idx = None; } else { close_angle_bracket_idx = Some(i); } }; if push_text && text_start != i { nodes.push(Node::Text(&input[text_start..i])); } } Context::Placeholder(ph_start) => { if c == '{' && i == ph_start + 1 { // Double curly brackets "{{": context = Context::Text(ph_start); } else if c == '}' { // The end of a placeholder: nodes.push(Node::Placeholder(&input[ph_start..i + 1])); context = Context::Text(i + 1); } } Context::Color(tag_start) => { if c == '<' && i == tag_start + 1 { // Double open angle brackets "<<": context = Context::Text(tag_start + 1); } else if c == '>' { // The end of a color code: let tag_input = &input[tag_start..i + 1]; let mut tag = parse::color_tag(tag_input) .map_err(|e| { use nom::Err; let (input, error) = match e { Err::Error(parse::Error { detail: Some(d), .. }) | Err::Failure(parse::Error { detail: Some(d), .. }) => { (d.input, Error::ParseTag(d.message)) } // Should never happen: _ => (tag_input, Error::UnableToParseTag(tag_input.to_string())), }; err!([input] error) })? .1; tag.set_span(span!(tag_input)); nb_open_tags += if tag.is_close { -1 } else { 1 }; // Group consecutive tags into one group, in order to improve optimization // (e.g., "" will be optimized by removing the useless "" // ANSI sequence): if let Some(Node::ColorTagGroup(last_tag_group)) = nodes.last_mut() { last_tag_group.push(tag); } else { nodes.push(Node::ColorTagGroup(vec![tag])); } context = Context::Text(i + 1); } } } } // Process the end of the string: match context { Context::Text(text_start) => { if text_start != input.len() { nodes.push(Node::Text(&input[text_start..])); } // Auto-close remaining open tags: if nb_open_tags > 0 { let tags = (0..nb_open_tags) .map(|_| ColorTag::new_close()) .collect::>(); nodes.push(Node::ColorTagGroup(tags)); } Ok(nodes) } Context::Placeholder(start) => Err(err!([&input[start..]] Error::UnclosedPlaceholder)), Context::Color(start) => Err(err!([&input[start..]] Error::UnclosedTag)), } } color-print-proc-macro-0.3.7/src/lib.rs000064400000000000000000000150161046102023000160510ustar 00000000000000//! This internal library provides the procedural macros needed by the crate [`color-print`]. //! //! [`color-print`]: https://crates.io/crates/color-print extern crate proc_macro; #[macro_use] mod util; #[cfg(not(feature = "terminfo"))] mod ansi; #[cfg(not(feature = "terminfo"))] mod ansi_constants; mod color_context; mod error; mod format_args; mod parse; #[cfg(feature = "terminfo")] mod terminfo; mod untagged; use proc_macro::TokenStream; use quote::{quote, ToTokens}; use syn::{ parse::{Parse, ParseStream}, parse_macro_input, token::Comma, Expr, }; /// The same as `format!()`, but parses color tags. /// /// #### Example /// /// ``` /// # use color_print_proc_macro::cformat; /// let s: String = cformat!("A green word, {}", "placeholders are allowed"); /// assert_eq!(s, "A \u{1b}[32mgreen\u{1b}[39m word, placeholders are allowed"); /// ``` #[proc_macro] #[cfg(not(feature = "terminfo"))] pub fn cformat(input: TokenStream) -> TokenStream { get_macro("format", input, false) } /// The same as `format!()`, but parses color tags. #[proc_macro] #[cfg(feature = "terminfo")] pub fn cformat(input: TokenStream) -> TokenStream { get_macro("format", input, false) } /// The same as `print!()`, but parses color tags. /// /// #### Example /// /// ``` /// # use color_print_proc_macro::cprint; /// cprint!("A green word, {}", "placeholders are allowed"); /// ``` #[proc_macro] #[cfg(not(feature = "terminfo"))] pub fn cprint(input: TokenStream) -> TokenStream { get_macro("print", input, false) } /// The same as `print!()`, but parses color tags. #[proc_macro] #[cfg(feature = "terminfo")] pub fn cprint(input: TokenStream) -> TokenStream { get_macro("print", input, false) } /// The same as `eprint!()`, but parses color tags. /// /// #### Example /// /// ``` /// # use color_print_proc_macro::ceprint; /// ceprint!("A green word, {}", "placeholders are allowed"); /// ``` #[proc_macro] #[cfg(not(feature = "terminfo"))] pub fn ceprint(input: TokenStream) -> TokenStream { get_macro("eprint", input, false) } /// The same as `eprint!()`, but parses color tags. #[proc_macro] #[cfg(feature = "terminfo")] pub fn ceprint(input: TokenStream) -> TokenStream { get_macro("eprint", input, false) } /// The same as `println!()`, but parses color tags. /// /// #### Example /// /// ``` /// # use color_print_proc_macro::cprintln; /// cprintln!("A green word, {}", "placeholders are allowed"); /// ``` #[proc_macro] #[cfg(not(feature = "terminfo"))] pub fn cprintln(input: TokenStream) -> TokenStream { get_macro("println", input, false) } /// The same as `println!()`, but parses color tags. #[proc_macro] #[cfg(feature = "terminfo")] pub fn cprintln(input: TokenStream) -> TokenStream { get_macro("println", input, false) } /// The same as `eprintln!()`, but parses color tags. /// /// #### Example /// /// ``` /// # use color_print_proc_macro::ceprintln; /// ceprintln!("A green word, {}", "placeholders are allowed"); /// ``` #[proc_macro] #[cfg(not(feature = "terminfo"))] pub fn ceprintln(input: TokenStream) -> TokenStream { get_macro("eprintln", input, false) } /// The same as `eprintln!()`, but parses color tags. #[proc_macro] #[cfg(feature = "terminfo")] pub fn ceprintln(input: TokenStream) -> TokenStream { get_macro("eprintln", input, false) } /// The same as `write!()`, but parses color tags. #[proc_macro] #[cfg(not(feature = "terminfo"))] pub fn cwrite(input: TokenStream) -> TokenStream { get_macro("write", input, true) } /// The same as `write!()`, but parses color tags. #[proc_macro] #[cfg(feature = "terminfo")] pub fn cwrite(input: TokenStream) -> TokenStream { get_macro("write", input, true) } /// The same as `writeln!()`, but parses color tags. #[proc_macro] #[cfg(not(feature = "terminfo"))] pub fn cwriteln(input: TokenStream) -> TokenStream { get_macro("writeln", input, true) } /// The same as `writeln!()`, but parses color tags. #[proc_macro] #[cfg(feature = "terminfo")] pub fn cwriteln(input: TokenStream) -> TokenStream { get_macro("writeln", input, true) } /// Colorizes a string literal, without formatting the `format!`-like placeholders. /// /// * Accepts only one argument; /// * Will panic if feature `terminfo` is activated. /// /// #### Example /// /// ``` /// # use color_print_proc_macro::cstr; /// let s: &str = cstr!("A green word"); /// assert_eq!(s, "A \u{1b}[32mgreen\u{1b}[39m word"); /// ``` #[cfg(not(feature = "terminfo"))] #[proc_macro] pub fn cstr(input: TokenStream) -> TokenStream { crate::ansi::get_cstr(input) .unwrap_or_else(|err| err.to_token_stream()) .into() } /// Removes all the color tags from the given string literal. /// /// Accepts only one argument. /// /// #### Example /// /// ``` /// # use color_print_proc_macro::untagged; /// let s: &str = untagged!("A normal word"); /// assert_eq!(s, "A normal word"); /// ``` #[proc_macro] pub fn untagged(input: TokenStream) -> TokenStream { crate::untagged::get_untagged(input) .unwrap_or_else(|err| err.to_token_stream()) .into() } /// Colorizes a string literal, without formatting the `format!`-like placeholders. /// /// * Accepts only one argument; /// * Will panic if feature `terminfo` is activated. #[cfg(feature = "terminfo")] #[proc_macro] pub fn cstr(_: TokenStream) -> TokenStream { panic!("Macro cstr!() cannot be used with terminfo feature") } struct WriteInput { dst: Expr, rest: TokenStream, } impl Parse for WriteInput { fn parse(input: ParseStream) -> syn::parse::Result { let dst: Expr = input.parse()?; let _: Comma = input.parse()?; let rest = input.parse_terminated(Expr::parse, Comma)?; let rest = quote! { #rest }.into(); // Not sure how to do best? Ok(Self { dst, rest }) } } /// Renders a whole processed macro. fn get_macro(macro_name: &str, input: TokenStream, is_write_macro: bool) -> TokenStream { let macro_name = util::ident(macro_name); let fmt_args = |input_tail| { #[cfg(not(feature = "terminfo"))] let format_args = crate::ansi::get_format_args(input_tail); #[cfg(feature = "terminfo")] let format_args = crate::terminfo::get_format_args(input_tail); format_args.unwrap_or_else(|err| err.to_token_stream()) }; if is_write_macro { let WriteInput { dst, rest } = parse_macro_input!(input); let format_args = fmt_args(rest); (quote! { #macro_name!(#dst, #format_args) }).into() } else { let format_args = fmt_args(input); (quote! { #macro_name!(#format_args) }).into() } } color-print-proc-macro-0.3.7/src/parse/color_tag.rs000064400000000000000000000345171046102023000203750ustar 00000000000000use std::borrow::Cow; use nom::{ Err, branch::alt, bytes::complete::{tag, take_while_m_n}, character::complete::{space0, alphanumeric1, alpha1, u8, digit1}, combinator::{consumed, map, map_res}, multi::separated_list1, sequence::{tuple, delimited, preceded, pair, terminated}, error::ErrorKind, }; use super::{Input, Result, Error, Parser, ErrorDetail}; use super::util::*; use crate::color_context::{ Change, ChangeSet, Color, Color16, Color256, ColorRgb, ColorTag, ColorKind, BaseColor, Intensity, }; /// Indicates wether a colored is specified by the prefix "fg:" or "bg:". #[derive(Debug, Clone, Copy)] enum Specified { True, False, } impl Specified { #[inline] fn is_true(&self) -> bool { matches!(self, Specified::True) } } /// Indicates a color has to be searched in its lowercase or uppercase version. #[derive(Debug, Clone, Copy)] enum Case { Uppercase, Lowercase, } /// Parses a color tag. pub fn color_tag(input: Input<'_>) -> Result<'_, ColorTag> { let tag = alt(( map( tuple((tag(""))), |_| (true, vec![]) ), delimited( tag("<"), alt(( map( preceded(tag("/"), spaced(separated_list1(stag(","), spaced(attr)))), |attrs| (true, attrs) ), map( separated_list1(stag(","), spaced(attr)), |attrs| (false, attrs) ), )), tag(">"), ), )); with_failure_message( map( consumed(tag), |(source, (is_close, changes))| ColorTag { source: Some(source), span: None, is_close, change_set: ChangeSet::from(changes.as_ref()), } ), "Unable to parse this tag" )(input) } /// Parses any attributes inside a color tag. fn attr(input: Input<'_>) -> Result<'_, Change> { let mut parser = alt(( style_attr, map(tuple((color_kind_specifier, specified_color)), |(kind, color)| kind.to_change(color)), map(color_16(Case::Lowercase), |color_16| Change::Foreground(Color::Color16(color_16))), map( color_256(Specified::False), |(color_256, color_kind)| color_kind.unwrap().to_change(Color::Color256(color_256)) ), map( color_rgb(Specified::False), |(color_rgb, color_kind)| color_kind.unwrap().to_change(Color::ColorRgb(color_rgb)) ), map(color_16(Case::Uppercase), |color_16| Change::Background(Color::Color16(color_16))), )); parser(input).map_err(|e| { match e { Err::Error(_) => { let msg = match alphanumeric1::<&str, Error>(input) { Ok((_, attr)) => format!("Unknown color attribute: <{attr}>"), Err(_) => "Unable to parse this attribute".to_string(), }; Err::Failure(Error::new(input, ErrorKind::Alpha, Some(ErrorDetail::new(input, msg)))) } e => e } }) } /// Parses a style attribute. fn style_attr(input: Input<'_>) -> Result<'_, Change> { let (input, word) = alpha1(input)?; let change = match word { "s" | "strong" | "bold" | "em" => Change::Bold, "dim" => Change::Dim, "u" | "underline" => Change::Underline, "i" | "italic" | "italics" => Change::Italics, "blink" => Change::Blink, "strike" => Change::Strike, "reverse" | "rev" => Change::Reverse, "conceal" | "hide" => Change::Conceal, _ => { return Err(Err::Error(Error::new(input, ErrorKind::Tag, None))) } }; Ok((input, change)) } /// Parses specifiers like `"bg:"`. fn color_kind_specifier(input: Input<'_>) -> Result<'_, ColorKind> { check_parser_before_failure( pair(spaced(alpha1), stag(":")), terminated( alt(( map(word(alt((tag("fg"), tag("f")))), |_| ColorKind::Foreground), map(word(alt((tag("bg"), tag("b")))), |_| ColorKind::Background), )), stag(":"), ), "Unknown specifier, allowed specifiers are \"bg\" or \"fg\" (shortcuts: \"b\" or \"f\")" ) (input) } /// Parses a color which has been prefixed by a specifier like `"bg:"` or `"fg:"`. fn specified_color(input: Input<'_>) -> Result<'_, Color> { with_failure_message( alt(( map(color_16(Case::Lowercase), Color::Color16), map(color_256(Specified::True), |(color, _)| Color::Color256(color)), map(color_rgb(Specified::True), |(color, _)| Color::ColorRgb(color)), )), "Unknown color" ) (input) } /// Parses a basic color like `"blue"`, `"b"`, `"blue!"`, `"bright-blue"`, with the given letter /// case. fn color_16<'a>(letter_case: Case) -> impl Parser<'a, Color16> { move |input| { let bright_prefix = match letter_case { Case::Uppercase => "BRIGHT-", Case::Lowercase => "bright-", }; alt(( map( preceded(tag(bright_prefix), base_color(letter_case)), |base_color| Color16::new(base_color, Intensity::Bright) ), map( pair(spaced(base_color(letter_case)), is_present(spaced(tag("!")))), |(base_color, is_bright)| Color16::new(base_color, Intensity::new(is_bright)) ) )) (input) } } /// Parses a 256-color color, like `"pal(42)"`. If the color to parse is declared as "specified", /// the only the lowercase functions will be available. fn color_256<'a>(specified: Specified) -> impl Parser<'a, (Color256, Option)> { const PALETTE_FAILURE_MESSAGE: &str = "Palette color must a number between 0 and 255"; fn pal_color(input: Input<'_>) -> Result<'_, u8> { with_failure_message(u8, PALETTE_FAILURE_MESSAGE)(input) } fn pal_fn<'a>(name1: &'a str, name2: &'a str, name3: &'a str) -> impl Parser<'a, u8> { let function_names = alt((tag(name1), tag(name2), tag(name3))); function( function_names, with_failure_message(pal_color, PALETTE_FAILURE_MESSAGE) ) } fn pal_lower(input: Input<'_>) -> Result<'_, Color256> { map(alt(( pal_fn("palette", "pal", "p"), check_parser_before_failure(digit1, u8, PALETTE_FAILURE_MESSAGE) )), Color256)(input) } fn pal_upper(input: Input<'_>) -> Result<'_, Color256> { map(pal_fn("PALETTE", "PAL", "P"), Color256)(input) } if specified.is_true() { |input| { map(pal_lower, |color| (color, None)) (input) } } else { |input| { alt(( map(pal_lower, |color| (color, Some(ColorKind::Foreground))), map(pal_upper, |color| (color, Some(ColorKind::Background))) )) (input) } } } /// Parses a true-color color, like `"rgb(10,20,30)"`. If the color to parse is declared as /// "specified", the only the lowercase functions will be available. fn color_rgb<'a>(specified: Specified) -> impl Parser<'a, (ColorRgb, Option)> { fn component(input: Input<'_>) -> Result<'_, u8> { with_failure_message(u8, "Bad RGB color component: must be a number between 0 and 255") (input) } fn rgb_fn(name: &str) -> impl Parser<'_, ColorRgb> { map( function( tag(name), with_failure_message( tuple((component, stag(","), component, stag(","), component)), "Wrong arguments: expects 3 numbers between 0 and 255, separated by commas" ) ), |(r, _, g, _, b)| ColorRgb { r, g, b } ) } fn rgb_lower(input: Input<'_>) -> Result<'_, ColorRgb> { rgb_fn("rgb")(input) } fn rgb_upper(input: Input<'_>) -> Result<'_, ColorRgb> { rgb_fn("RGB")(input) } if specified.is_true() { |input| { map(alt((rgb_lower, hex_rgb_color)), |color| (color, None)) (input) } } else { |input| { alt(( map(rgb_lower, |color| (color, Some(ColorKind::Foreground))), map(rgb_upper, |color| (color, Some(ColorKind::Background))), map(hex_rgb_color, |color| (color, Some(ColorKind::Foreground))), )) (input) } } } /// Parses an HTML-like color like `"#aabbcc"`. fn hex_rgb_color(input: Input<'_>) -> Result<'_, ColorRgb> { fn component(input: Input<'_>) -> Result<'_, u8> { map_res( take_while_m_n(2, 2, |c: char| c.is_ascii_hexdigit()), |input| u8::from_str_radix(input, 16) ) (input) } map( preceded( tag("#"), with_failure_message( tuple((component, component, component)), "Bad hexadecimal color code" ) ), |(r, g ,b)| ColorRgb { r, g, b } ) (input) } /// Parses a base color name, like "blue", "red", in the given letter case. fn base_color<'a>(letter_case: Case) -> impl Parser<'a, BaseColor> { move |input| { let (input, word) = match letter_case { Case::Uppercase => { let (input, word) = uppercase_word(input)?; (input, Cow::Owned(word.to_ascii_lowercase())) } Case::Lowercase => { let (input, word) = lowercase_word(input)?; (input, Cow::Borrowed(word)) } }; let base_color = match word.as_ref() { "k" | "black" => BaseColor::Black, "r" | "red" => BaseColor::Red, "g" | "green" => BaseColor::Green, "y" | "yellow" => BaseColor::Yellow, "b" | "blue" => BaseColor::Blue, "m" | "magenta" => BaseColor::Magenta, "c" | "cyan" => BaseColor::Cyan, "w" | "white" => BaseColor::White, _ => { return Err(Err::Error(Error::new(input, ErrorKind::Tag, None))) } }; Ok((input, base_color)) } } #[cfg(test)] mod tests { use super::*; use crate::color_context::{Color, Color16, BaseColor, Intensity}; macro_rules! tag { ($source:expr, $is_close:expr, $($changes:expr),*) => { ColorTag { source: Some($source), span: None, is_close: $is_close, change_set: ChangeSet::from(&[$($changes),*][..]), } }; } macro_rules! open_tag { ($source:expr, [ $($changes:expr),* $(,)? ]) => { tag!($source, false, $($changes),*) }; } macro_rules! close_tag { ($source:expr, [ $($changes:expr),* $(,)? ]) => { tag!($source, true, $($changes),*) }; } macro_rules! color16 { ($base_color:ident, $intensity:ident) => { Color::Color16(Color16::new(BaseColor::$base_color, Intensity::$intensity)) } } #[test] fn parse_change() { let change = attr("b").unwrap().1; assert_eq!(change, Change::Foreground(color16!(Blue, Normal))); let change = attr("s").unwrap().1; assert_eq!(change, Change::Bold); } #[test] fn parse_tag() { let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [Change::Bold])); let tag = color_tag("...").unwrap().1; assert_eq!( tag, open_tag!( "", [ Change::Bold, Change::Foreground(color16!(Yellow, Bright)), ] ) ); let tag = color_tag("...").unwrap().1; assert_eq!( tag, close_tag!( "", [ Change::Underline, Change::Foreground(color16!(Black, Normal)), Change::Background(color16!(Blue, Normal)), ] ) ); } #[test] fn parse_color256() { let tag = color_tag("<48>").unwrap().1; assert_eq!(tag, open_tag!("<48>", [Change::Foreground(Color::Color256(Color256(48)))])); let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [Change::Foreground(Color::Color256(Color256(48)))])); let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [Change::Background(Color::Color256(Color256(48)))])); let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [Change::Background(Color::Color256(Color256(48)))])); } #[test] fn parse_color_rgb() { let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [ Change::Foreground(Color::ColorRgb(ColorRgb{ r: 1, g: 2, b: 3})) ])); let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [ Change::Background(Color::ColorRgb(ColorRgb{ r: 1, g: 2, b: 3})) ])); let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [ Change::Foreground(Color::ColorRgb(ColorRgb{ r: 1, g: 2, b: 3})) ])); let tag = color_tag("< #102030 >").unwrap().1; assert_eq!(tag, open_tag!("< #102030 >", [ Change::Foreground(Color::ColorRgb(ColorRgb{ r: 16, g: 32, b: 48})) ])); } #[test] fn spaces_in_tag() { let tag = color_tag("").unwrap().1; assert_eq!(tag, open_tag!("", [Change::Bold])); let tag = color_tag("< s>").unwrap().1; assert_eq!(tag, open_tag!("< s>", [Change::Bold])); let tag = color_tag("< s > ...").unwrap().1; assert_eq!(tag, open_tag!("< s >", [Change::Bold])); let tag = color_tag("< s , \t y!>...").unwrap().1; assert_eq!( tag, open_tag!( "< s , \t y!>", [ Change::Bold, Change::Foreground(color16!(Yellow, Bright)), ] ) ); } #[test] fn empty_tag_is_err() { assert!(color_tag("<>").is_err()); assert!(color_tag("< >").is_err()); } } color-print-proc-macro-0.3.7/src/parse/mod.rs000064400000000000000000000002141046102023000171660ustar 00000000000000mod types; mod color_tag; mod util; pub use color_tag::color_tag; pub use types::{Error, ErrorDetail}; use types::{Input, Result, Parser}; color-print-proc-macro-0.3.7/src/parse/types.rs000064400000000000000000000034021046102023000175550ustar 00000000000000use std::fmt; use nom::{ IResult, error::{ParseError, FromExternalError, ErrorKind}, }; pub type Input<'a> = &'a str; pub type Result<'a, V> = IResult, V, Error<'a>>; pub trait Parser<'a, V>: FnMut(Input<'a>) -> Result<'a, V> {} impl<'a, V, F> Parser<'a, V> for F where F: FnMut(Input<'a>) -> Result<'a, V> {} #[derive(Debug, PartialEq, Clone)] pub struct ErrorDetail<'a> { pub input: &'a str, pub message: String, } impl<'a> ErrorDetail<'a> { pub fn new(input: &'a str, message: impl Into) -> Self { let input = &input[..input.len().min(1)]; Self { input, message: message.into() } } } impl<'a> fmt::Display for ErrorDetail<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.message) } } /// Replacement to [`nom::error::Error`]. #[derive(Debug, PartialEq)] pub struct Error<'a> { pub input: Input<'a>, pub code: ErrorKind, pub detail: Option>, } impl<'a> Error<'a> { pub fn new(input: &'a str, code: ErrorKind, detail: Option>) -> Self { Error { input, code, detail } } pub fn with_detail(&self, detail: ErrorDetail<'a>) -> Self { Error { input: self.input, code: self.code, detail: Some(detail) } } } /// Mandatory [`ParseError`] implementation. impl<'a> ParseError> for Error<'a> { fn from_error_kind(input: Input<'a>, kind: ErrorKind) -> Self { Error { input, code: kind, detail: None } } fn append(_: Input<'a>, _: ErrorKind, other: Self) -> Self { other } } impl<'a, E> FromExternalError, E> for Error<'a> { fn from_external_error(input: Input<'a>, kind: ErrorKind, _e: E) -> Self { Error { input, code: kind, detail: None } } } color-print-proc-macro-0.3.7/src/parse/util.rs000064400000000000000000000074431046102023000173770ustar 00000000000000use nom::{ Err, sequence::{delimited, preceded}, character::complete::{multispace0, alpha1}, bytes::complete::tag, combinator::{map, opt}, error::ErrorKind, }; use super::{Parser, Result, Input, Error, ErrorDetail}; /// Transforms an error into a failure, while adding a message in the error detail. pub fn with_failure_message<'a, P, V>(mut parser: P, message: &'a str) -> impl Parser<'a, V> where P: Parser<'a, V>, { move |input: Input<'a>| parser(input).map_err( |nom_err: Err| match nom_err { Err::Error(e) => { Err::Failure(e.with_detail(ErrorDetail::new(input, message))) } e => e, } ) } /// Checks if the first parser succeeds, then parses the input with the second parser. If an error /// is encountered with the second parser, then a failure message is thrown. pub fn check_parser_before_failure<'a, C, CV, P, PV>( mut check_parser: C, mut parser: P, failure_msg: &'a str ) -> impl Parser<'a, PV> where C: Parser<'a, CV>, P: Parser<'a, PV>, { move |input| { check_parser(input)?; with_failure_message(|input| { parser(input) }, failure_msg) (input) } } /// Creates a parser which accpets spaces around the original parsed input. pub fn spaced<'a, P, V>(parser: P) -> impl Parser<'a, V> where P: Parser<'a, V>, { delimited( multispace0, parser, multispace0, ) } /// Parsed a spaced tag. pub fn stag(s: &str) -> impl Parser<'_, &str> { spaced(tag(s)) } /// Creates a parser which makes the parser optional and returns true if the parse was successful. pub fn is_present<'a, P, V>(parser: P) -> impl Parser<'a, bool> where P: Parser<'a, V>, { map(opt(parser), |v| v.is_some()) } /// Creates a parser which parses a function call. pub fn function<'a, PV, N, P>(word_parser: N, parser: P) -> impl Parser<'a, PV> where N: Parser<'a, &'a str>, P: Parser<'a, PV>, { preceded( word(word_parser), delimited( with_failure_message(stag("("), "Missing opening brace"), parser, with_failure_message(stag(")"), "Missing closing brace") ) ) } /// Parses a word made only by alpha characters ('a' => 'z' and 'A' => 'Z'), and checks if this /// word matches exactly the given parser. pub fn word<'a, P>(mut word_parser: P) -> impl Parser<'a, &'a str> where P: Parser<'a, &'a str>, { move |input| { let (input, word) = alpha1(input)?; match word_parser(word) { Ok((_, parsed_word)) => { if word == parsed_word { Ok((input, word)) } else { Err(Err::Error(Error::new(input, ErrorKind::Alpha, None))) } } Err(e) => Err(e), } } } /// Parses an uppercase word. pub fn uppercase_word(input: Input<'_>) -> Result<'_, &str> { let (input, word) = alpha1(input)?; if word.chars().all(|c| c.is_ascii_uppercase()) { Ok((input, word)) } else { Err(Err::Error(Error::new(input, ErrorKind::Alpha, None))) } } /// Parses a lowercase word. pub fn lowercase_word(input: Input<'_>) -> Result<'_, &str> { let (input, word) = alpha1(input)?; if word.chars().all(|c| c.is_ascii_lowercase()) { Ok((input, word)) } else { Err(Err::Error(Error::new(input, ErrorKind::Alpha, None))) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_uppercase_word() { let input = "foo"; assert!(uppercase_word(input).is_err()); let input = "FOOfoo"; assert!(uppercase_word(input).is_err()); let input = "FOO"; assert!(uppercase_word(input).is_ok()); let input = "FOO;;"; assert!(uppercase_word(input).is_ok()); } } color-print-proc-macro-0.3.7/src/terminfo.rs000064400000000000000000000062561046102023000171340ustar 00000000000000use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::{quote, ToTokens}; use syn::LitStr; use crate::color_context::Context; use crate::error::SpanError; use crate::format_args::{get_args_and_format_string, parse_format_string, Node}; use crate::util; /// Common code shared between the public macros, terminfo implementation. pub fn get_format_args(input: TokenStream) -> Result { let (format_string_token, args) = get_args_and_format_string(input)?; let format_string = format_string_token.value(); // Split the format string into a list of nodes; each node is either a string literal (text), a // placeholder for a `format!()` related macro, or a color code: let format_nodes = parse_format_string(&format_string, &format_string_token)?; // The final, modified format string which will be given to a `format!()`-like macro: let mut final_format_string = String::new(); // Stores which colors and attributes are set while processing the format string: let mut color_context = Context::new(); // Used to generate extra named arguments: let mut current_color_idx = 0; // The list of the extra named arguments to add at the end of the `format!`-like macro: let mut color_format_args: Vec = vec![]; // Generate the final format string, and construct the list of the extra named parameters at // the same time: for node in format_nodes { match node { Node::Text(s) | Node::Placeholder(s) => { final_format_string.push_str(s); } Node::ColorTagGroup(tag_group) => { let constants = color_context .terminfo_apply_tags(tag_group)? .iter() .map(|s| constant_to_token_stream(s)) .collect::>(); for constant in constants { // Add "{}" to the format string, and add the right ANSI sequence as a format // argument: let varname = format!("__color_print__color_{}", current_color_idx); final_format_string.push_str(&format!("{{{}}}", varname)); current_color_idx += 1; let varname_ident = util::ident(&varname); let token_stream = quote! { #varname_ident = #constant }.into(); color_format_args.push(token_stream); } } } } // Group all the final arguments into a single iterator: let format_string_span = format_string_token.span(); let final_format_string = LitStr::new(&final_format_string, format_string_span).to_token_stream(); let final_args = std::iter::once(final_format_string) .chain(args.iter().map(|arg| arg.to_token_stream()).skip(1)) .chain(color_format_args.into_iter()); Ok((quote! { #(#final_args),* }).into()) } /// Creates a new terminfo constant (available in the `color-print` package) as a token stream. fn constant_to_token_stream(constant: &str) -> TokenStream2 { let constant_ident = util::ident(constant); (quote! { *color_print::#constant_ident }).into() } color-print-proc-macro-0.3.7/src/untagged.rs000064400000000000000000000035011046102023000170750ustar 00000000000000//! Implements the [`crate::untagged!()`] proc macro. use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use crate::color_context::Context; use crate::error::{SpanError, Error}; use crate::format_args::{ parse_args, get_format_string, parse_format_string, Node }; /// Transforms a string literal by removing all its color tags. pub fn get_untagged(input: TokenStream) -> Result { let args = parse_args(input)?; let format_string_token = get_format_string(args.first())?; let format_string = format_string_token.value(); if args.len() > 1 { return Err(SpanError::new(Error::TooManyArgs, None)); } // Split the format string into a list of nodes; each node is either a string literal (text), // or a color code; `format!`-like placeholders will be parsed indenpendently, but as they are // put back unchanged into the format string, it's not a problem: let format_nodes = parse_format_string(&format_string, &format_string_token)?; // The final, modified format string which will be given to the `format!`-like macro: let mut format_string = String::new(); // Stores which colors and attributes are set while processing the format string: let mut color_context = Context::new(); // Generate the final format string: for node in format_nodes { match node { Node::Text(s) | Node::Placeholder(s) => { format_string.push_str(s); } Node::ColorTagGroup(tag_group) => { // Don't add the ansi codes into the final format string, but still apply to tags // to the context in order to keep the error handling: color_context.apply_tags(tag_group)?; } } } Ok(quote! { #format_string }) } color-print-proc-macro-0.3.7/src/util.rs000064400000000000000000000032071046102023000162570ustar 00000000000000use std::ops::RangeBounds; use proc_macro2::{Ident, Span}; use syn::LitStr; /// Joins the arguments with `&&` operators. macro_rules! and { ($($expr:expr),* $(,)?) => { $($expr)&&* }; } /// Joins the arguments with `||` operators. #[cfg(feature = "terminfo")] macro_rules! or { ($($expr:expr),* $(,)?) => { $($expr)||* }; } /// Creates a new [`Ident`] which can be tokenized. pub fn ident(s: &str) -> Ident { Ident::new(s, Span::call_site()) } /// Creates a new [`struct@LitStr`] which can be tokenized. pub fn literal_string(s: &str) -> LitStr { LitStr::new(s, Span::call_site()) } /// Unfortunately, unless a nightly compiler is used, this function will actually only return the /// original input span. /// /// Returns the subspan corresponding to the range of `inside` inside `input`, considering that: /// - `input` is exactly `&input_lit_str.value()`, /// - `inside` is a subslice of `input`, /// /// Warning: may panic if the conditions are not met. /// TODO: improve safety pub fn inner_span<'a>(input: &'a str, input_lit_str: &LitStr, inside: &'a str) -> Span { let input_offset = (inside.as_ptr() as usize) - (input.as_ptr() as usize); let range = input_offset + 1..input_offset + inside.len() + 1; subspan(input_lit_str.span(), range).unwrap_or_else(|| input_lit_str.span()) } /// Returns a subspan of the given span. /// /// TODO: the implementation is really... wtf! But i didn't find a better way to do it. fn subspan>(span: Span, range: R) -> Option { let mut lit = proc_macro2::Literal::i8_suffixed(0); // wtf... lit.set_span(span); lit.subspan(range) }