// TAMER formatting helpers // // Copyright (C) 2014-2023 Ryan Specialty, LLC. // // This file is part of TAME. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . //! Typed formatting helpers. //! //! These types create composable formatters for use with [`Display`]. //! Whereas [`Display`] operates on data owned by the type implementing it, //! these formatters compose into functions that operate on data provided //! _to_ it. //! Consequently, //! formatters are simply types, //! and writes can be streamed just as they are with [`Display`]. //! //! There are two types of wrappers: //! //! - [`DisplayWrapper`] formats objects as atoms; and //! - [`ListDisplayWrapper`] maps a [`DisplayWrapper`] to each of its //! items as atoms, //! where the specific wrapper used depends on the position of the //! item within the list and the properties of the list itself. //! //! [`DisplayWrapper::wrap`] and [`ListDisplayWrapper::wrap`] can be used to //! associate wrappers with data, //! effectively creating a custom [`Display`] implementation controlled //! by the caller. //! This is _not_ a substitute for canonical object representations that own //! their own [`Display`] implementation, //! but is suitable where display is context-dependent. //! //! For example: //! //! ``` //! # use tamer::{fmt::*, xir::fmt::*}; //! let attrs = ["foo", "bar", "baz"]; //! //! assert_eq!( //! AndQualConjList::<"attribute", "attributes", Tt>::wrap(&attrs) //! .to_string(), //! "attributes `@foo`, `@bar`, and `@baz`", //! ); //! //! assert_eq!( //! AndQualConjList::<"attribute", "attributes", Tt>::wrap(&attrs[0..1]) //! .to_string(), //! "attribute `@foo`", //! ); //! //! assert_eq!( //! AndConjList::::wrap(&["toil", "trouble"]).to_string(), //! "toil and trouble", //! ); //! ``` //! //! This is in contrast to the approach taken by (for example) //! the [diagnostic system](crate::diagnose), //! which is to produce a data structure representing the data to be //! formatted. //! This system is not useful as, //! and is not intended to be, //! an IR for more sophisticated manipulation of data prior to output. use std::{ fmt::{Display, Formatter, Result}, marker::PhantomData, }; /// Wrapper for a [`Display`]-able type. /// /// See the [module-level documentation](super) for more information. pub trait DisplayWrapper { /// Transform inner data and output using the provided [`Formatter`]. /// /// If a [`Formatter`] is not available, /// [`ListDisplayWrapper::wrap`] may be used to produce a /// [`Display`]-able object instead. fn fmt(inner: T, f: &mut Formatter) -> Result; /// Associate data with a [`DisplayWrapper`] for rendering using /// [`Display`]. /// /// This has the effect of creating an arbitrary [`Display`] /// implementation for the wrapped object, /// which will work well with [`format!`] and anything else that /// does not have access to an explicit [`Formatter`]. fn wrap(inner: T) -> Wrap { Wrap { inner, _phantom: Default::default(), } } } /// Wrapper with associated data. /// /// This has the effect of creating an arbitrary [`Display`] implementation /// for the wrapped data, /// which will work well with [`format!`] and anything else that does /// not have access to an explicit [`Formatter`]. pub struct Wrap { inner: T, _phantom: PhantomData, } impl Display for Wrap { fn fmt(&self, f: &mut Formatter) -> Result { W::fmt(&self.inner, f) } } /// Echo data as-is without any wrapping. /// /// This is primarily used at the root of a wrapper composition. pub struct Raw; impl DisplayWrapper for Raw { fn fmt(inner: T, f: &mut Formatter) -> Result { inner.fmt(f) } } /// Prefix data with a static [`str`]. /// /// See also [`Suffix`] and [`Delim`]. pub struct Prefix( PhantomData, ); impl DisplayWrapper for Prefix { fn fmt(inner: T, f: &mut Formatter) -> Result { f.write_str(PREFIX)?; W::fmt(inner, f) } } /// Suffix data with a static [`str`]. /// /// See also [`Prefix`] and [`Delim`]. pub struct Suffix( PhantomData, ); impl DisplayWrapper for Suffix { fn fmt(inner: T, f: &mut Formatter) -> Result { W::fmt(inner, f)?; f.write_str(SUFFIX) } } /// Surround a value in delimiters. /// /// See also [`Prefix`] and [`Suffix`]. pub type Delim = Prefix>; /// Denote text that would conventionally be delimited in a teletypewriter /// font. /// /// This produces a markdown-style quote using backticks. /// /// NB: This does not defend against nested quotes, /// so this is _not_ safe against format escapes. pub type Tt = Delim<"`", "`", W>; /// Quote text that would conventionally be delimited in a teletypewriter /// font. /// /// This is a more terse alternative to [`Tt`] when formatter composition is /// unneeded. pub type TtQuote = Tt; /// Prefix with a single space. pub type Sp = Prefix<" ", W>; /// Wrapper for a list that maps each element to a context-specific /// [`DisplayWrapper`]. /// /// This uses the slice API for wrapping since [`Display`] takes objects by /// non-mutable reference, /// and so we cannot consume an iterator. /// /// The associated types define the wrappers to use for items in various /// positions depending on the length of the list. /// They were chosen to be somewhat intuitive given the necessary use cases. /// /// See the [module-level documentation](super) for examples. pub trait ListDisplayWrapper { /// Wrapper to use when the list contains only a single item. type Single: DisplayWrapper = Raw; /// Wrapper for the first item in a multi-item list. type First: DisplayWrapper = Raw; /// Wrapper for all but the first and last items in a multi-item list. type Middle: DisplayWrapper = Raw; /// Wrapper for the last item of a list containing a pair of items. type LastOfPair: DisplayWrapper = Raw; /// Wrapper for the last item of a list containing more than two items. type LastOfMany: DisplayWrapper = Raw; /// Format a slice using the provided wrappers. /// /// If a [`Formatter`] is not available, /// [`ListDisplayWrapper::wrap`] may be used to produce a /// [`Display`]-able object instead. fn fmt(list: &[T], f: &mut Formatter) -> Result { let lasti = list.len().saturating_sub(1); // This can be further abstracted away using the above primitives, // if ever we have a use. for next in list.iter().enumerate() { match next { (i, x) => Self::fmt_nth(lasti, i, x, f)?, }; } Ok(()) } /// Format an item as if it were the `i`th value of a list of length /// `lasti+1`. /// /// This allows for generating list-like output without the expense of /// actually producing a list. /// This may be useful when values are stored in different memory /// location, /// so that the displaying of those values is a problem of invoking /// this method on them in the right order, /// rather than collecting them just for the sake of display. /// If Rust supports `const` array/Vec functions in the future, /// this may not be necessary anymore, /// unless we also don't want the space cost of such a /// precomputation /// (but it may come with performance benefits from locality). #[inline] fn fmt_nth( lasti: usize, i: usize, item: &T, f: &mut Formatter, ) -> Result { match (i, item) { (0, x) if lasti == 0 => { Self::Single::fmt(x, f)?; } (0, x) => { Self::First::fmt(x, f)?; } (i, x) if lasti == i => { if i == 1 { Self::LastOfPair::fmt(x, f)?; } else { Self::LastOfMany::fmt(x, f)?; } } (_, x) => { Self::Middle::fmt(x, f)?; } } Ok(()) } /// Associate data with a [`ListDisplayWrapper`] for rendering using /// [`Display`]. /// /// This has the effect of creating an arbitrary [`Display`] /// implementation for the wrapped slice, /// which will work well with [`format!`] and anything else that /// does not have access to an explicit [`Formatter`]. fn wrap(list: &[T]) -> ListWrap { ListWrap { list, _phantom: Default::default(), } } } /// Format each item of a slice using a [`DisplayWrapper`] formatter, /// outputting an English list with a serial comma and conjunctive term. /// /// No formatting is done to a single item, /// and the serial comma is omitted for only two items. /// It is assumed that the items are not complete sentences, /// and nested lists are not expected /// (in such a case you'd want to replace the inner list with /// semicolons and a more robust abstraction is needed). /// /// The use of the serial comma (also known as the Oxford comma) is the /// preference of the author. /// /// For example: /// If we have a slice `[1, 2, 3]`, /// this will output "1, 2, and 3". /// If we have a slice `[1, 2]`, /// it will omit the serial comma and output "1 and 2", /// since no person would write "1, and 2" unless they wished to /// place particularly dramatic emphasis on the first item. /// /// To output a qualifier before the list, /// see [`QualConjList`]. pub struct ConjList( PhantomData, ); impl ListDisplayWrapper for ConjList { type Single = W; type First = W; type Middle = Prefix<", ", W>; type LastOfPair = Sp>>; // Comma after the penultimate term (serial/Oxford comma). type LastOfMany = Prefix<", ", Prefix>>; } /// A list of values with a serial comma and the term "and" as a /// conjunction between the penultimate and final items. pub type AndConjList = ConjList<"and", W>; /// A list of values with a serial comma and the term "or" as a /// conjunction between the penultimate and final items. /// /// Terminology note: /// English refers to the term "or" here as a conjunction between words, /// which differs from "or" in logic as a disjunction. pub type OrConjList = ConjList<"or", W>; /// Format each item of a slice using a [`DisplayWrapper`] formatter, /// outputting an English list with a serial comma and conjunctive term, /// qualified by a term that is singular or plural depending on the length /// of the list. /// /// See [`ConjList`] for more information; /// this operates identically but with the addition of the qualifier. pub struct QualConjList< const QUAL_ONE: &'static str, const QUAL_MANY: &'static str, const CONJ: &'static str, W: DisplayWrapper, >(PhantomData); impl< const QUAL_ONE: &'static str, const QUAL_MANY: &'static str, const CONJ: &'static str, W: DisplayWrapper, > ListDisplayWrapper for QualConjList { type Single = Prefix as ListDisplayWrapper>::Single>>; type First = Prefix as ListDisplayWrapper>::First>>; type Middle = as ListDisplayWrapper>::Middle; type LastOfPair = as ListDisplayWrapper>::LastOfPair; type LastOfMany = as ListDisplayWrapper>::LastOfMany; } /// A list of values with a serial comma and the term "and" as a /// conjunction between the penultimate and final items. /// prefixed with a singular or plural qualifier term depending on the /// length of the list. pub type AndQualConjList< const QUAL_ONE: &'static str, const QUAL_MANY: &'static str, W, > = QualConjList; /// A list of values with a serial comma and the term "or" as a /// conjunction between the penultimate and final items. /// prefixed with a singular or plural qualifier term depending on the /// length of the list. /// /// Terminology note: /// English refers to the term "or" here as a conjunction between words, /// which differs from "or" in logic as a disjunction. pub type OrQualConjList< const QUAL_ONE: &'static str, const QUAL_MANY: &'static str, W, > = QualConjList; /// A list of values separated by a delimiter. /// /// This is analogous to the typical `join` operation on lists. /// Single-element lists will have no delimiter, /// but lists of multiple elements will have each element delimited by the /// same delimiter. /// This is in contrast with wrappers that follow more sophisticated /// typographical conventions, /// such as [`ConjList`] and [`QualConjList`]. pub struct JoinListWrap( PhantomData, ); impl ListDisplayWrapper for JoinListWrap { type Single = W; type First = W; type Middle = Prefix; type LastOfPair = Prefix; type LastOfMany = Prefix; } /// List wrapper with associated data. /// /// This has the effect of creating an arbitrary [`Display`] implementation /// for the wrapped list, /// which will work well with [`format!`] and anything else that does /// not have access to an explicit [`Formatter`]. pub struct ListWrap<'a, W: ListDisplayWrapper + ?Sized, T: Display> { list: &'a [T], _phantom: PhantomData, } impl<'a, W: ListDisplayWrapper + ?Sized, T: Display> Display for ListWrap<'a, W, T> { fn fmt(&self, f: &mut Formatter) -> Result { W::fmt(self.list, f) } } /// Wrap a `fmt`-like function to be used as [`Display::fmt`] for this /// object. /// /// This works around the problem of having a function expecting a /// [`Formatter`], /// but not having a [`Formatter`] to call it with. /// It also allows for arbitrary (compatible) functions to be used as /// [`Display`]. pub struct DisplayFn Result>(pub F); impl Result> Display for DisplayFn { fn fmt(&self, f: &mut Formatter) -> Result { (self.0)(f) } } #[cfg(test)] mod test { use super::*; #[test] fn prefix() { assert_eq!(Prefix::<"@!", Raw>::wrap("foo").to_string(), "@!foo",); assert_eq!( Prefix::<"1", Prefix::<"2", Raw>>::wrap("bar").to_string(), "12bar", ); } #[test] fn suffix() { assert_eq!(Suffix::<"!@", Raw>::wrap("foo").to_string(), "foo!@",); assert_eq!( Suffix::<"1", Suffix::<"2", Raw>>::wrap("bar").to_string(), "bar21", ); } #[test] fn delimit() { assert_eq!(Delim::<"[", "]", Raw>::wrap("foo").to_string(), "[foo]",); assert_eq!( Delim::<"(", ")", Delim::<"|", "|", Raw>>::wrap("bar").to_string(), "(|bar|)", ); } // Certain types are trivially verifiable by their definition, // and so have no tests. #[test] fn conj_list_single() { assert_eq!( ConjList::<"unused", Raw>::wrap(&["single"]).to_string(), "single", ); // Ensure that we're actually mapping. assert_eq!( ConjList::<"unused", Delim<"(", ")", Raw>>::wrap(&["single"]) .to_string(), "(single)", ); } #[test] fn conj_list_double() { assert_eq!( ConjList::<"and", Raw>::wrap(&["first", "second"]).to_string(), "first and second", ); // Ensure that we're actually mapping. assert_eq!( ConjList::<"or", Delim<"(", ")", Raw>>::wrap(&["first", "second"]) .to_string(), "(first) or (second)", ); } #[test] fn conj_list_many() { assert_eq!( ConjList::<"and", Raw>::wrap(&["first", "second", "third"]) .to_string(), "first, second, and third", ); // Ensure that we're actually mapping. assert_eq!( ConjList::<"or", Delim<"[", "]", Raw>>::wrap(&[ "first", "second", "third" ]) .to_string(), "[first], [second], or [third]", ); } #[test] fn qualified_conj_list_single() { assert_eq!( QualConjList::<"thing", "things", "unused", Raw>::wrap(&["a"]) .to_string(), "thing a", ); // Ensure that we're actually mapping. assert_eq!( QualConjList::<"thing", "things", "unused", Delim<"(", ")", Raw>>::wrap(&["a"]) .to_string(), "thing (a)", ); } #[test] fn qualified_conj_list_double() { assert_eq!( QualConjList::<"thing", "things", "and", Raw>::wrap(&["a", "b"]) .to_string(), "things a and b", ); // Ensure that we're actually mapping. assert_eq!( QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b"]) .to_string(), "things (a) or (b)", ); } #[test] fn qualified_conj_list_many() { assert_eq!( QualConjList::<"thing", "things", "and", Raw>::wrap(&[ "a", "b", "c" ]) .to_string(), "things a, b, and c", ); // Ensure that we're actually mapping. assert_eq!( QualConjList::<"thing", "things", "or", Delim<"(", ")", Raw>>::wrap(&["a", "b", "c"]) .to_string(), "things (a), (b), or (c)", ); } #[test] fn display_fn() { assert_eq!( DisplayFn(|f| write!(f, "test fmt")).to_string(), "test fmt", ); } // `fmt_nth` is used by the above tests, // but that's an implementation detail; // we expose it as a public API so it ought to be tested too. #[test] fn fmt_nth() { type Sut = QualConjList<"thing", "things", "or", Raw>; assert_eq!( DisplayFn(|f| Sut::fmt_nth(0, 0, &"foo", f)).to_string(), "thing foo", ); assert_eq!( DisplayFn(|f| Sut::fmt_nth(1, 0, &"foo", f)).to_string(), "things foo", ); assert_eq!( DisplayFn(|f| Sut::fmt_nth(1, 1, &"foo", f)).to_string(), " or foo", ); assert_eq!( DisplayFn(|f| Sut::fmt_nth(2, 0, &"foo", f)).to_string(), "things foo", ); assert_eq!( DisplayFn(|f| Sut::fmt_nth(2, 1, &"foo", f)).to_string(), ", foo", ); assert_eq!( DisplayFn(|f| Sut::fmt_nth(2, 2, &"foo", f)).to_string(), ", or foo", ); } #[test] fn delim_list() { assert_eq!( JoinListWrap::<"::", Raw>::wrap(&["one", "two", "three"]) .to_string(), "one::two::three", ); assert_eq!( JoinListWrap::<" -> ", TtQuote>::wrap(&["foo", "bar"]).to_string(), "`foo` -> `bar`", ); } }