bpaf/
buffer.rs

1#[cfg(feature = "docgen")]
2use crate::{
3    info::Info,
4    meta_help::{HelpItem, HelpItems},
5};
6use crate::{
7    item::{Item, ShortLong},
8    Meta,
9};
10
11mod console;
12mod html;
13#[cfg(feature = "docgen")]
14mod manpage;
15mod splitter;
16
17pub(crate) use self::console::Color;
18use self::console::MAX_WIDTH;
19
20#[cfg(feature = "docgen")]
21pub use manpage::Section;
22
23impl From<&[(&str, Style)]> for Doc {
24    fn from(val: &[(&str, Style)]) -> Self {
25        let mut res = Doc::default();
26        for (text, style) in val {
27            res.write_str(text, *style);
28        }
29        res
30    }
31}
32
33impl<const N: usize> From<&'static [(&'static str, Style); N]> for Doc {
34    fn from(val: &'static [(&'static str, Style); N]) -> Self {
35        let mut res = Doc::default();
36        for (text, style) in val {
37            res.write_str(text, *style);
38        }
39        res
40    }
41}
42
43/// Parser metainformation
44///
45///
46/// This is a newtype around internal parser metainfo representation, generated
47/// with [`Parser::with_group_help`](crate::Parser::with_group_help) and consumed by
48/// [`Doc::meta`](Doc::meta)
49#[derive(Copy, Clone)]
50pub struct MetaInfo<'a>(pub(crate) &'a Meta);
51
52impl Doc {
53    #[inline]
54    /// Append a fragment of plain text to [`Doc`]
55    ///
56    /// See [`Doc`] for usage examples
57    pub fn text(&mut self, text: &str) {
58        self.write_str(text, Style::Text);
59    }
60
61    #[inline]
62    /// Append a fragment of literal text to [`Doc`]
63    ///
64    /// See [`Doc`] for usage examples
65    pub fn literal(&mut self, text: &str) {
66        self.write_str(text, Style::Literal);
67    }
68
69    #[inline]
70    /// Append a fragment of text with emphasis to [`Doc`]
71    ///
72    /// See [`Doc`] for usage examples
73    pub fn emphasis(&mut self, text: &str) {
74        self.write_str(text, Style::Emphasis);
75    }
76
77    #[inline]
78    /// Append a fragment of unexpected user input to [`Doc`]
79    ///
80    /// See [`Doc`] for usage examples
81    pub fn invalid(&mut self, text: &str) {
82        self.write_str(text, Style::Invalid);
83    }
84
85    /// Append a fragment of parser metadata to [`Doc`]
86    ///
87    /// See [`Doc`] for usage examples
88    pub fn meta(&mut self, meta: MetaInfo, for_usage: bool) {
89        self.write_meta(meta.0, for_usage);
90    }
91
92    /// Append a `Doc` to [`Doc`]
93    ///
94    /// See [`Doc`] for usage examples
95    pub fn doc(&mut self, buf: &Doc) {
96        self.tokens.push(Token::BlockStart(Block::InlineBlock));
97        self.tokens.extend(&buf.tokens);
98        self.payload.push_str(&buf.payload);
99        self.tokens.push(Token::BlockEnd(Block::InlineBlock));
100    }
101
102    /// Append a `Doc` to [`Doc`] for plaintext documents try to format
103    /// first line as a help section header
104    pub fn em_doc(&mut self, buf: &Doc) {
105        self.tokens.push(Token::BlockStart(Block::InlineBlock));
106        if let Some(Token::Text {
107            bytes,
108            style: Style::Text,
109        }) = buf.tokens.first().copied()
110        {
111            let prefix = &buf.payload[0..bytes];
112            if let Some((a, b)) = prefix.split_once('\n') {
113                self.emphasis(a);
114                self.tokens.push(Token::BlockStart(Block::Section3));
115                self.text(b);
116
117                if buf.tokens.len() > 1 {
118                    self.tokens.extend(&buf.tokens[1..]);
119                    self.payload.push_str(&buf.payload[bytes..]);
120                }
121                self.tokens.push(Token::BlockEnd(Block::Section3));
122            } else {
123                self.emphasis(prefix);
124            }
125        } else {
126            self.tokens.extend(&buf.tokens);
127            self.payload.push_str(&buf.payload);
128        }
129
130        self.tokens.push(Token::BlockEnd(Block::InlineBlock));
131    }
132}
133
134impl Doc {
135    pub(crate) fn write_shortlong(&mut self, name: &ShortLong) {
136        match name {
137            ShortLong::Short(s) => {
138                self.write_char('-', Style::Literal);
139                self.write_char(*s, Style::Literal);
140            }
141            ShortLong::Long(l) | ShortLong::Both(_, l) => {
142                self.write_str("--", Style::Literal);
143                self.write_str(l, Style::Literal);
144            }
145        }
146    }
147
148    pub(crate) fn write_item(&mut self, item: &Item) {
149        match item {
150            Item::Positional { metavar, help: _ } => {
151                self.metavar(*metavar);
152            }
153            Item::Command {
154                name: _,
155                short: _,
156                help: _,
157                meta: _,
158                info: _,
159            } => {
160                self.write_str("COMMAND ...", Style::Metavar);
161            }
162            Item::Flag {
163                name,
164                shorts: _,
165                env: _,
166                help: _,
167            } => self.write_shortlong(name),
168            Item::Argument {
169                name,
170                shorts: _,
171                metavar,
172                env: _,
173                help: _,
174            } => {
175                self.write_shortlong(name);
176                self.write_char('=', Style::Text);
177                self.metavar(*metavar);
178            }
179            Item::Any {
180                metavar,
181                anywhere: _,
182                help: _,
183            } => {
184                self.doc(metavar);
185            }
186        }
187    }
188
189    pub(crate) fn write_meta(&mut self, meta: &Meta, for_usage: bool) {
190        fn go(meta: &Meta, f: &mut Doc) {
191            match meta {
192                Meta::And(xs) => {
193                    for (ix, x) in xs.iter().enumerate() {
194                        if ix != 0 {
195                            f.write_str(" ", Style::Text);
196                        }
197                        go(x, f);
198                    }
199                }
200                Meta::Or(xs) => {
201                    for (ix, x) in xs.iter().enumerate() {
202                        if ix != 0 {
203                            f.write_str(" | ", Style::Text);
204                        }
205                        go(x, f);
206                    }
207                }
208                Meta::Optional(m) => {
209                    f.write_str("[", Style::Text);
210                    go(m, f);
211                    f.write_str("]", Style::Text);
212                }
213                Meta::Required(m) => {
214                    f.write_str("(", Style::Text);
215                    go(m, f);
216                    f.write_str(")", Style::Text);
217                }
218                Meta::Item(i) => f.write_item(i),
219                Meta::Many(m) => {
220                    go(m, f);
221                    f.write_str("...", Style::Text);
222                }
223
224                Meta::Adjacent(m) | Meta::Subsection(m, _) | Meta::Suffix(m, _) => {
225                    go(m, f);
226                }
227                Meta::Skip => {} // => f.write_str("no parameters expected", Style::Text),
228                Meta::CustomUsage(_, u) => {
229                    f.doc(u);
230                }
231                Meta::Strict(m) => {
232                    f.write_str("--", Style::Literal);
233                    f.write_str(" ", Style::Text);
234                    go(m, f);
235                }
236            }
237        }
238
239        let meta = meta.normalized(for_usage);
240        self.token(Token::BlockStart(Block::Mono));
241        go(&meta, self);
242        self.token(Token::BlockEnd(Block::Mono));
243    }
244}
245
246/// Style of a text fragment inside of [`Doc`]
247#[derive(Debug, Copy, Clone, Eq, PartialEq)]
248#[non_exhaustive]
249pub enum Style {
250    /// Plain text, no decorations
251    Text,
252
253    /// Word with emphasis - things like "Usage", "Available options", etc
254    Emphasis,
255
256    /// Something user needs to type literally - command names, etc
257    Literal,
258
259    /// Metavavar placeholder - something user needs to replace with own input
260    Metavar,
261
262    /// Invalid input given by user - used to display invalid parts of the input
263    Invalid,
264}
265
266#[derive(Debug, Copy, Clone, Eq, PartialEq)]
267#[allow(dead_code)]
268pub(crate) enum Block {
269    /// level 1 section header, block for separate command inside manpage, not used in --help
270    Header,
271
272    Section2,
273
274    // 2 margin
275    /// level 3 section header, "group_help" header, etc.
276    Section3,
277
278    // inline, 4 margin, no nesting
279    /// -h, --help
280    ItemTerm,
281
282    // widest term up below 20 margin margin plus two, but at least 4.
283    /// print usage information, but also items inside Numbered/Unnumbered lists
284    ItemBody,
285
286    // 0 margin
287    /// Definition list,
288    DefinitionList,
289
290    /// block of text, blocks are separated by a blank line in man or help
291    /// can contain headers or other items inside
292    Block,
293
294    /// inserted when block is written into a block. single blank line in the input
295    /// fast forward until the end of current inline block
296    InlineBlock,
297
298    // inline
299    /// displayed with `` in monochrome or not when rendered with colors.
300    /// In markdown this becomes a link to a term if one is defined
301    TermRef,
302
303    /// Surrounds metavars block in manpage
304    ///
305    /// used only inside render_manpage at the moment
306    Meta,
307
308    /// Monospaced font that goes around [`Meta`]
309    Mono,
310}
311
312#[derive(Debug, Copy, Clone)]
313pub(crate) enum Token {
314    Text { bytes: usize, style: Style },
315    BlockStart(Block),
316    BlockEnd(Block),
317}
318
319#[derive(Debug, Clone, Default)]
320/// String with styled segments.
321///
322/// You can add style information to generated documentation and help messages
323/// For simpliest possible results you can also pass a string slice in all the places
324/// that require `impl Into<Doc>`
325pub struct Doc {
326    /// string info saved here
327    payload: String,
328
329    /// string meta info tokens
330    tokens: Vec<Token>,
331}
332
333impl std::fmt::Display for Doc {
334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335        let width = f.width().unwrap_or(MAX_WIDTH);
336        f.write_str(&self.render_console(true, Color::Monochrome, width))
337    }
338}
339
340#[derive(Debug, Clone, Copy, Default)]
341struct Skip(usize);
342impl Skip {
343    fn enabled(self) -> bool {
344        self.0 > 0
345    }
346    fn enable(&mut self) {
347        self.0 = 1;
348    }
349    fn push(&mut self) {
350        if self.0 > 0 {
351            self.0 += 1;
352        }
353    }
354    fn pop(&mut self) {
355        self.0 = self.0.saturating_sub(1);
356    }
357}
358
359impl Doc {
360    pub(crate) fn is_empty(&self) -> bool {
361        self.tokens.is_empty()
362    }
363
364    pub(crate) fn first_line(&self) -> Option<Doc> {
365        if self.tokens.is_empty() {
366            return None;
367        }
368
369        let mut res = Doc::default();
370        let mut cur = 0;
371
372        for &t in &self.tokens {
373            match t {
374                Token::Text { bytes, style } => {
375                    let payload = &self.payload[cur..cur + bytes];
376                    if let Some((first, _)) = payload.split_once('\n') {
377                        res.tokens.push(Token::Text {
378                            bytes: first.len(),
379                            style,
380                        });
381                        res.payload.push_str(first);
382                    } else {
383                        res.tokens.push(t);
384                        res.payload.push_str(&self.payload[cur..cur + bytes]);
385                        cur += bytes;
386                    }
387                }
388                _ => break,
389            }
390        }
391        Some(res)
392    }
393
394    #[cfg(feature = "autocomplete")]
395    pub(crate) fn to_completion(&self) -> Option<String> {
396        let mut s = self.first_line()?.monochrome(false);
397        s.truncate(s.trim_end().len());
398        Some(s)
399    }
400}
401
402impl From<&str> for Doc {
403    fn from(value: &str) -> Self {
404        let mut buf = Doc::default();
405        buf.write_str(value, Style::Text);
406        buf
407    }
408}
409
410impl Doc {
411    //    #[cfg(test)]
412    //    pub(crate) fn clear(&mut self) {
413    //        self.payload.clear();
414    //        self.tokens.clear();
415    //    }
416
417    #[inline(never)]
418    pub(crate) fn token(&mut self, token: Token) {
419        self.tokens.push(token);
420    }
421
422    pub(crate) fn write<T>(&mut self, input: T, style: Style)
423    where
424        T: std::fmt::Display,
425    {
426        use std::fmt::Write;
427        let old_len = self.payload.len();
428        let _ = write!(self.payload, "{}", input);
429        self.set_style(self.payload.len() - old_len, style);
430    }
431
432    #[inline(never)]
433    fn set_style(&mut self, len: usize, style: Style) {
434        // buffer chunks are unified with previous chunks of the same type
435        // [metavar]"foo" + [metavar]"bar" => [metavar]"foobar"
436        match self.tokens.last_mut() {
437            Some(Token::Text {
438                bytes: prev_bytes,
439                style: prev_style,
440            }) if *prev_style == style => {
441                *prev_bytes += len;
442            }
443            _ => {
444                self.tokens.push(Token::Text { bytes: len, style });
445            }
446        }
447    }
448
449    #[inline(never)]
450    pub(crate) fn write_str(&mut self, input: &str, style: Style) {
451        self.payload.push_str(input);
452        self.set_style(input.len(), style);
453    }
454
455    #[inline(never)]
456    pub(crate) fn write_char(&mut self, c: char, style: Style) {
457        self.write_str(c.encode_utf8(&mut [0; 4]), style);
458    }
459}
460
461#[cfg(feature = "docgen")]
462#[derive(Debug, Clone)]
463struct DocSection<'a> {
464    /// Path name to the command name starting from the application
465    path: Vec<String>,
466    info: &'a Info,
467    meta: &'a Meta,
468}
469
470#[cfg(feature = "docgen")]
471fn extract_sections<'a>(
472    meta: &'a Meta,
473    info: &'a Info,
474    path: &mut Vec<String>,
475    sections: &mut Vec<DocSection<'a>>,
476) {
477    sections.push(DocSection {
478        path: path.clone(),
479        info,
480        meta,
481    });
482    let mut hi = HelpItems::default();
483    hi.append_meta(meta);
484    for item in &hi.items {
485        if let HelpItem::Command {
486            name,
487            short: _,
488            help: _,
489            meta,
490            info,
491        } = item
492        {
493            path.push((*name).to_string());
494            extract_sections(meta, info, path, sections);
495            path.pop();
496        }
497    }
498}