pretty_xmlish/
lib.rs

1//! How to use this library? Very easy!
2//!
3//! ```rust
4//! use pretty_xmlish::{Pretty, PrettyConfig};
5//! use std::collections::BTreeMap;
6//! // This class controls the expected width, indent size, and more.
7//! let mut config = PrettyConfig::default();
8//! // Other factory methods are available
9//! let pretty = Pretty::simple_record("BatchNestedLoopJoin",
10//!     BTreeMap::new(), // fields, if any
11//!     vec![] // children, if any
12//! );
13//! let mut out = String::with_capacity(114514);
14//! let w = config.unicode(&mut out, &pretty);
15//! // w is the width of the output
16//! // output is stored in `out`
17//! ```
18
19use std::{
20    borrow::Cow,
21    fmt::{Debug, Display},
22    iter::repeat,
23};
24
25pub type Str<'a> = Cow<'a, str>;
26/// This is recommended by `@rami3l` to supercede `Vec<Pretty>`.
27/// Why not use this? Because Rust wouldn't let me!
28/// https://github.com/rust-lang/rust/issues/23714
29#[allow(dead_code)]
30type Pretties<'a> = Cow<'a, [Pretty<'a>]>;
31/// Good kids don't do this.
32type BTreeMap<K, V> = Vec<(K, V)>;
33/// Cow-str associative array
34pub type CowAssocArr<'a> = BTreeMap<Str<'a>, Pretty<'a>>;
35pub type StrAssocArr<'a> = BTreeMap<&'a str, Pretty<'a>>;
36
37pub mod ascii;
38pub mod unicode;
39
40pub mod helper;
41
42#[derive(Clone)]
43pub struct XmlNode<'a> {
44    pub name: Str<'a>,
45    pub fields: CowAssocArr<'a>,
46    /// Currently, if fields have `XmlNode` with children,
47    /// they will not be considered during linearization.
48    pub(crate) fields_is_linear: bool,
49    pub children: Vec<Pretty<'a>>,
50}
51
52impl<'a> XmlNode<'a> {
53    pub fn simple_record(
54        name: impl Into<Str<'a>>,
55        fields: StrAssocArr<'a>,
56        children: Vec<Pretty<'a>>,
57    ) -> Self {
58        let name = name.into();
59        let fields = fields.into_iter().map(|(k, v)| (k.into(), v)).collect();
60        Self::new(name, fields, children)
61    }
62
63    pub fn has_children(&self) -> bool {
64        !self.children.is_empty() || (self.fields.iter()).any(|(_, x)| x.has_children())
65    }
66
67    fn ol_build_str_ascii(&self, reduced_ws: bool, builder: &mut String) {
68        builder.push_str(&self.name);
69        if self.fields.is_empty() {
70            return;
71        }
72        builder.push_str(" { ");
73        for (i, (k, v)) in self.fields.iter().enumerate() {
74            if i > 0 {
75                builder.push_str(", ");
76            }
77            builder.push_str(k);
78            builder.push_str(": ");
79            v.ol_build_str_ascii(reduced_ws, builder);
80        }
81        builder.push_str(" }");
82    }
83
84    fn ol_len(&self, reduced_ws: bool) -> usize {
85        let mem: usize = (self.fields.iter())
86            .map(|(k, v)| k.chars().count() + ": ".len() + v.ol_len(reduced_ws))
87            .sum();
88        let mid = self.fields.len().saturating_sub(1) * ", ".len();
89        let begin_end = if self.fields.is_empty() {
90            0
91        } else {
92            " {  }".len()
93        } + self.name.chars().count();
94        mem + mid + begin_end
95    }
96
97    pub fn new(name: Str<'a>, fields: CowAssocArr<'a>, children: Vec<Pretty<'a>>) -> Self {
98        Self {
99            name,
100            fields,
101            fields_is_linear: false,
102            children,
103        }
104    }
105}
106
107/// Use `into`!!
108#[derive(Clone)]
109pub enum Pretty<'a> {
110    Text(Str<'a>),
111    Record(XmlNode<'a>),
112    Array(Vec<Self>),
113    Linearized(&'a Self, usize),
114}
115
116impl<'a> Pretty<'a> {
117    pub fn simple_record(
118        name: impl Into<Str<'a>>,
119        fields: StrAssocArr<'a>,
120        children: Vec<Self>,
121    ) -> Self {
122        Self::Record(XmlNode::simple_record(name, fields, children))
123    }
124    // Blame Rust for not having named arguments and default values.
125    pub fn fieldless_record(name: impl Into<Str<'a>>, children: Vec<Self>) -> Self {
126        Self::simple_record(name, Default::default(), children)
127    }
128    pub fn childless_record(name: impl Into<Str<'a>>, fields: StrAssocArr<'a>) -> Self {
129        Self::simple_record(name, fields, Default::default())
130    }
131    pub fn list_of_strings(list: &'a [&'a str]) -> Self {
132        Self::Array(list.iter().map(|&s| s.into()).collect())
133    }
134
135    pub fn display(display: &impl Display) -> Self {
136        display.to_string().into()
137    }
138
139    pub fn debug(debug: &impl Debug) -> Self {
140        format!("{:?}", debug).into()
141    }
142
143    pub fn has_children(&self) -> bool {
144        use Pretty::*;
145        match self {
146            Record(xml) => xml.has_children(),
147            Array(v) => v.iter().any(Self::has_children),
148            Text(..) => false,
149            // Note: linearization happens only when children are absent
150            Linearized(..) => false,
151        }
152    }
153
154    /// Potential improvements: instead of using a mutable String,
155    /// use a Write trait with monadic error reporting.
156    pub(crate) fn ol_build_str_ascii(&self, reduced_ws: bool, builder: &mut String) {
157        use Pretty::*;
158        match self {
159            Text(s) => builder.push_str(s),
160            Record(xml) => xml.ol_build_str_ascii(reduced_ws, builder),
161            Array(v) => {
162                if v.is_empty() {
163                    builder.push_str("[]");
164                    return;
165                }
166                builder.push('[');
167                if !reduced_ws {
168                    builder.push(' ');
169                }
170                for (i, e) in v.iter().enumerate() {
171                    if i > 0 {
172                        builder.push_str(", ");
173                    }
174                    e.ol_build_str_ascii(reduced_ws, builder);
175                }
176                if !reduced_ws {
177                    builder.push(' ');
178                }
179                builder.push(']');
180            }
181            Linearized(p, _) => p.ol_build_str_ascii(reduced_ws, builder),
182        }
183    }
184
185    pub fn to_one_line_string(&self, reduced_ws: bool) -> String {
186        let mut builder = String::with_capacity(self.ol_len(reduced_ws));
187        self.ol_build_str_ascii(reduced_ws, &mut builder);
188        builder
189    }
190
191    /// Does not include children of records.
192    pub(crate) fn ol_len(&self, reduced_ws: bool) -> usize {
193        use Pretty::*;
194        match self {
195            Text(s) => s.chars().count(),
196            Record(xml) => xml.ol_len(reduced_ws),
197            Array(v) => {
198                if v.is_empty() {
199                    return "[]".len();
200                }
201                let mem: usize = v.iter().map(|x| x.ol_len(reduced_ws)).sum();
202                let mid = (v.len() - 1) * ", ".len();
203                let beg = if reduced_ws { "[]".len() } else { "[  ]".len() };
204                mem + mid + beg
205            }
206            Linearized(_, len) => *len,
207        }
208    }
209}
210
211impl<'a, T: Into<Str<'a>>> From<T> for Pretty<'a> {
212    fn from(s: T) -> Self {
213        Pretty::Text(s.into())
214    }
215}
216
217#[derive(Clone)]
218pub struct PrettyConfig {
219    pub indent: usize,
220    /// Preferred width of the output, exlusive of the boundaries.
221    pub width: usize,
222    pub need_boundaries: bool,
223    /// If true, then there will not be space before record name and enclosed
224    /// in lists.
225    pub reduced_spaces: bool,
226}
227
228impl PrettyConfig {
229    pub fn horizon(&self, out: &mut String, width: usize) {
230        if !self.need_boundaries {
231            return;
232        }
233        out.push_str("+");
234        out.extend(repeat("-").take(width + 2));
235        out.push_str("+");
236    }
237}
238
239struct LinedBuffer<'a> {
240    width: usize,
241    /// Modify when out is also modified.
242    pub already_occupied: usize,
243    out: &'a mut String,
244    config: &'a PrettyConfig,
245}
246impl<'a> LinedBuffer<'a> {
247    fn begin_line(&mut self) {
248        if self.config.need_boundaries {
249            self.out.push_str("| ");
250        }
251    }
252    fn push(&mut self, s: &str) {
253        self.out.push_str(s);
254        self.already_occupied += s.chars().count();
255    }
256    fn pip(&mut self, amount: usize) {
257        self.push(" ".repeat(amount).as_str());
258    }
259    fn pusheen(&mut self) {
260        // if self.width < self.already_occupied {
261        //     println!(
262        //         "Bug!! w: {}, ao: {}",
263        //         self.width, self.already_occupied
264        //     );
265        //     self.push(" |\n");
266        // } else {
267        let eol = if self.config.need_boundaries {
268            self.pip(self.width - self.already_occupied);
269            " |\n"
270        } else {
271            "\n"
272        };
273        self.push(eol);
274        // }
275        self.already_occupied = 0;
276    }
277}
278
279impl Default for PrettyConfig {
280    fn default() -> Self {
281        Self {
282            indent: 4,
283            width: 120,
284            need_boundaries: true,
285            reduced_spaces: false,
286        }
287    }
288}