dia_args/
docs.rs

1/*
2==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--
3
4Dia-Args
5
6Copyright (C) 2018-2019, 2021-2025  Anonymous
7
8There are several releases over multiple years,
9they are listed as ranges, such as: "2018-2019".
10
11This program is free software: you can redistribute it and/or modify
12it under the terms of the GNU Lesser General Public License as published by
13the Free Software Foundation, either version 3 of the License, or
14(at your option) any later version.
15
16This program is distributed in the hope that it will be useful,
17but WITHOUT ANY WARRANTY; without even the implied warranty of
18MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19GNU Lesser General Public License for more details.
20
21You should have received a copy of the GNU Lesser General Public License
22along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
24::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
25*/
26
27//! # Kit for documentation
28//!
29//! ## Examples
30//!
31//! ```
32//! use std::borrow::Cow;
33//! use dia_args::docs::{self, Cfg, Cmd, Docs, I18n, Option, Project};
34//!
35//! const CMD_HELP: &str = "help";
36//! const CMD_HELP_DOCS: Cow<str> = Cow::Borrowed("Prints help and exits.");
37//!
38//! const CMD_VERSION: &str = "version";
39//! const CMD_VERSION_DOCS: Cow<str> = Cow::Borrowed("Prints version and exits.");
40//!
41//! const CMD_DO_SOMETHING: &str = "do-something";
42//! const CMD_DO_SOMETHING_DOCS: Cow<str> = Cow::Borrowed(concat!(
43//!     "This command does something.\n",
44//!     "\n",
45//!     "It might NOT do that thing. If you encounter any problems,",
46//!     " please contact developers.\n",
47//! ));
48//!
49//! const OPTION_THIS: &[&str] = &["-t", "--this"];
50//! const OPTION_THIS_DEFAULT: bool = true;
51//! const OPTION_THIS_DOCS: Cow<str> = Cow::Borrowed("This argument has 2 names.");
52//!
53//! const OPTION_THAT: &[&str] = &["--that"];
54//! const OPTION_THAT_VALUES: &[u8] = &[99, 100];
55//! const OPTION_THAT_DEFAULT: u8 = OPTION_THAT_VALUES[0];
56//! const OPTION_THAT_DOCS: Cow<str> = Cow::Borrowed("This argument has 1 single name.");
57//!
58//! let help_cmd = Cmd::new(CMD_HELP, CMD_HELP_DOCS, None);
59//! let version_cmd = Cmd::new(CMD_VERSION, CMD_VERSION_DOCS, None);
60//! let do_something_cmd = Cmd::new(
61//!     CMD_DO_SOMETHING, CMD_DO_SOMETHING_DOCS,
62//!     Some(dia_args::make_options![
63//!         Option::new(OPTION_THIS, false, &[], Some(OPTION_THIS_DEFAULT), OPTION_THIS_DOCS),
64//!         Option::new(OPTION_THAT, true, OPTION_THAT_VALUES, Some(OPTION_THAT_DEFAULT), OPTION_THAT_DOCS),
65//!     ]),
66//! );
67//!
68//! let mut docs = Docs::new(
69//!     // Name
70//!     "The-Program".into(),
71//!     // Docs
72//!     "This is the Program".into(),
73//! );
74//! docs.commands = Some(dia_args::make_cmds![help_cmd, version_cmd, do_something_cmd,]);
75//! docs.project = Some(Project::new(Some("https://project-home"), "Nice License", None));
76//! docs.print()?;
77//!
78//! // This does the same as above command:
79//! // println!("{}", docs);
80//!
81//! # Ok::<_, std::io::Error>(())
82//! ```
83
84mod cfg;
85mod cmd;
86mod i18n;
87mod option;
88
89use {
90    core::{
91        fmt::{self, Display},
92        option::Option as RustOption,
93    },
94    std::borrow::Cow,
95};
96
97pub use self::{
98    cfg::*,
99    cmd::*,
100    i18n::*,
101    option::*,
102};
103
104#[cfg(unix)]
105const LINE_BREAK: &str = "\n";
106
107#[cfg(not(unix))]
108const LINE_BREAK: &str = "\r\n";
109
110/// # Makes a vector of [`Cow<'_, Cmd<'_>>`][struct:Cmd] from a list of either `Cmd` or `&Cmd`
111///
112/// [struct:Cmd]: docs/struct.Cmd.html
113#[macro_export]
114macro_rules! make_cmds {
115    ($($e: expr),+ $(,)?) => {{
116        vec!($(std::borrow::Cow::<dia_args::docs::Cmd>::from($e),)+)
117    }};
118}
119
120/// # Makes a vector of [`Cow<'_, Option<'_>>`][struct:Option] from a list of either `Option` or `&Option`
121///
122/// [struct:Option]: docs/struct.Option.html
123#[macro_export]
124macro_rules! make_options {
125    ($($e: expr),+ $(,)?) => {{
126        vec!($(std::borrow::Cow::<dia_args::docs::Option>::from($e),)+)
127    }};
128}
129
130/// # No values
131///
132/// This can be used conveniently with [`Option`][struct:Option].
133///
134/// ## Examples
135///
136/// ```
137/// use std::borrow::Cow;
138/// use dia_args::docs::{NO_VALUES, Option};
139///
140/// const OPTION_PORT: &[&str] = &["-p", "--port"];
141/// const OPTION_PORT_DOCS: Cow<str> = Cow::Borrowed("Port for server.");
142///
143/// let _option = Option::new(OPTION_PORT, false, NO_VALUES, None, OPTION_PORT_DOCS);
144/// ```
145///
146/// [struct:Option]: struct.Option.html
147pub const NO_VALUES: &[&str] = &[];
148
149/// # Documentation
150pub struct Docs<'a> {
151
152    /// # Name
153    pub name: Cow<'a, str>,
154
155    /// # Documentation
156    pub docs: Cow<'a, str>,
157
158    /// # Configuration
159    pub cfg: Cfg,
160
161    /// # Internatinonalization
162    pub i18n: I18n<'a>,
163
164    /// # Options
165    pub options: RustOption<Vec<Cow<'a, Option<'a>>>>,
166
167    /// # Commands
168    pub commands: RustOption<Vec<Cow<'a, Cmd<'a>>>>,
169
170    /// # Project
171    pub project: RustOption<Project<'a>>,
172
173}
174
175impl<'a> Docs<'a> {
176
177    /// # Makes new instance with default configurations
178    pub fn new(name: Cow<'a, str>, docs: Cow<'a, str>) -> Self {
179        Self {
180            name,
181            docs,
182            cfg: Cfg::default(),
183            i18n: I18n::default(),
184            options: None,
185            commands: None,
186            project: None,
187        }
188    }
189
190    /// # Prints this documentation to stdout
191    pub fn print(self) -> crate::Result<()> {
192        crate::lock_write_out(self.to_string().trim());
193        crate::lock_write_out([b'\n']);
194        Ok(())
195    }
196
197}
198
199impl Display for Docs<'_> {
200
201    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
202        let tab = self.cfg.tab_len().saturating_mul(self.cfg.tab_level().into());
203        let next_tab = self.cfg.tab_len().saturating_mul(self.cfg.tab_level().saturating_add(1).into());
204
205        // Name
206        f.write_str(&format(&self.name, tab, self.cfg.columns()))?;
207        f.write_str(LINE_BREAK)?;
208
209        // Docs
210        f.write_str(&format(&self.docs, next_tab, self.cfg.columns()))?;
211        f.write_str(LINE_BREAK)?;
212
213        // Options
214        f.write_str(&format(self.i18n.options.to_uppercase(), tab, self.cfg.columns()))?;
215        f.write_str(LINE_BREAK)?;
216        match self.options.as_ref() {
217            Some(options) => {
218                let cfg = self.cfg.increment_level();
219                for option in options {
220                    option.format(&cfg, &self.i18n, f)?;
221                }
222            },
223            None => {
224                f.write_str(&format(&self.i18n.no_options, next_tab, self.cfg.columns()))?;
225                f.write_str(LINE_BREAK)?;
226            },
227        };
228
229        // Commands
230        f.write_str(&format(self.i18n.commands.to_uppercase(), tab, self.cfg.columns()))?;
231        f.write_str(LINE_BREAK)?;
232        match self.commands.as_ref() {
233            Some(commands) => {
234                let cfg = self.cfg.increment_level();
235                for command in commands {
236                    command.format(&cfg, &self.i18n, f)?;
237                }
238            },
239            None => {
240                f.write_str(&format(&self.i18n.no_commands, next_tab, self.cfg.columns()))?;
241                f.write_str(LINE_BREAK)?;
242            },
243        };
244
245        // Project
246        if let Some(project) = self.project.as_ref() {
247            f.write_str(&format(self.i18n.project.to_uppercase(), tab, self.cfg.columns()))?;
248            f.write_str(LINE_BREAK)?;
249            if let Some(home) = project.home {
250                f.write_str(&format(format!("- {}: {}", self.i18n.home, home), next_tab, self.cfg.columns()))?;
251            }
252            f.write_str(&format(format!("- {}: {}", self.i18n.license, project.license_name), next_tab, self.cfg.columns()))?;
253            if let Some(license) = project.license.as_ref() {
254                f.write_str(LINE_BREAK)?;
255                f.write_str(&format(license, next_tab, self.cfg.columns()))?;
256            }
257        }
258
259        Ok(())
260    }
261
262}
263
264/// # Project information
265#[derive(Debug)]
266pub struct Project<'a> {
267    home: RustOption<&'a str>,
268    license_name: &'a str,
269    license: RustOption<Cow<'a, str>>,
270}
271
272impl<'a> Project<'a> {
273
274    /// # Makes new instance
275    pub const fn new(home: RustOption<&'a str>, license_name: &'a str, license: RustOption<Cow<'a, str>>) -> Self {
276        Self {
277            home,
278            license_name,
279            license,
280        }
281    }
282
283}
284
285/// # Formats a string
286fn format<S>(s: S, size_of_indentation: usize, columns: usize) -> String where S: AsRef<str> {
287    let s = s.as_ref();
288
289    if s.is_empty() || size_of_indentation >= columns {
290        return String::new();
291    }
292
293    let mut result = String::with_capacity(s.len().saturating_add(s.len() / 10));
294    let tab = concat!(' ').repeat(size_of_indentation);
295
296    for line in s.lines() {
297        let line_indentation = match line.split_whitespace().next() {
298            Some(word) => match word.chars().next() {
299                Some('-') | Some('+') | Some('*') | Some('~') | Some('$') | Some('#') =>
300                    Some(concat!(' ').repeat(word.chars().count().saturating_add(1))),
301                _ => None,
302            },
303            None => None,
304        };
305
306        let mut col = 0;
307        for (idx, word) in line.split_whitespace().enumerate() {
308            if idx == 0 {
309                result += &tab;
310                col = size_of_indentation;
311            }
312
313            let chars: Vec<_> = word.chars().collect();
314            if col + if col == size_of_indentation { 0 } else { 1 } + chars.len() <= columns {
315                if col > size_of_indentation {
316                    result.push(' ');
317                    col += 1;
318                }
319                col += chars.len();
320                result.extend(chars.into_iter());
321            } else {
322                for (i, c) in chars.into_iter().enumerate() {
323                    if i == 0 || col >= columns {
324                        result += LINE_BREAK;
325                        result += &tab;
326                        col = size_of_indentation;
327                        if let Some(line_indentation) = line_indentation.as_ref() {
328                            result += line_indentation;
329                            col += line_indentation.len();
330                        }
331                    }
332                    result.push(c);
333                    col += 1;
334                }
335            };
336        }
337
338        result += LINE_BREAK;
339    }
340
341    result
342}