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}