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#[derive(Copy, Clone)]
50pub struct MetaInfo<'a>(pub(crate) &'a Meta);
51
52impl Doc {
53 #[inline]
54 pub fn text(&mut self, text: &str) {
58 self.write_str(text, Style::Text);
59 }
60
61 #[inline]
62 pub fn literal(&mut self, text: &str) {
66 self.write_str(text, Style::Literal);
67 }
68
69 #[inline]
70 pub fn emphasis(&mut self, text: &str) {
74 self.write_str(text, Style::Emphasis);
75 }
76
77 #[inline]
78 pub fn invalid(&mut self, text: &str) {
82 self.write_str(text, Style::Invalid);
83 }
84
85 pub fn meta(&mut self, meta: MetaInfo, for_usage: bool) {
89 self.write_meta(meta.0, for_usage);
90 }
91
92 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 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 => {} 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
248#[non_exhaustive]
249pub enum Style {
250 Text,
252
253 Emphasis,
255
256 Literal,
258
259 Metavar,
261
262 Invalid,
264}
265
266#[derive(Debug, Copy, Clone, Eq, PartialEq)]
267#[allow(dead_code, clippy::enum_variant_names)]
268pub(crate) enum Block {
269 Header,
271
272 Section2,
273
274 Section3,
277
278 Section4,
281
282 ItemTerm,
285
286 ItemBody,
289
290 DefinitionList,
293
294 Block,
297
298 InlineBlock,
301
302 TermRef,
306
307 Meta,
311
312 Mono,
314}
315
316#[derive(Debug, Copy, Clone)]
317pub(crate) enum Token {
318 Text { bytes: usize, style: Style },
319 BlockStart(Block),
320 BlockEnd(Block),
321}
322
323#[derive(Debug, Clone, Default)]
324pub struct Doc {
330 payload: String,
332
333 tokens: Vec<Token>,
335}
336
337impl std::fmt::Display for Doc {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 let width = f.width().unwrap_or(MAX_WIDTH);
340 f.write_str(&self.render_console(true, Color::Monochrome, width))
341 }
342}
343
344#[derive(Debug, Clone, Copy, Default)]
345struct Skip(usize);
346impl Skip {
347 fn enabled(self) -> bool {
348 self.0 > 0
349 }
350 fn enable(&mut self) {
351 self.0 = 1;
352 }
353 fn push(&mut self) {
354 if self.0 > 0 {
355 self.0 += 1;
356 }
357 }
358 fn pop(&mut self) {
359 self.0 = self.0.saturating_sub(1);
360 }
361}
362
363impl Doc {
364 pub(crate) fn is_empty(&self) -> bool {
365 self.tokens.is_empty()
366 }
367
368 pub(crate) fn first_line(&self) -> Option<Doc> {
369 if self.tokens.is_empty() {
370 return None;
371 }
372
373 let mut res = Doc::default();
374 let mut cur = 0;
375
376 for &t in &self.tokens {
377 match t {
378 Token::Text { bytes, style } => {
379 let payload = &self.payload[cur..cur + bytes];
380 if let Some((first, _)) = payload.split_once('\n') {
381 res.tokens.push(Token::Text {
382 bytes: first.len(),
383 style,
384 });
385 res.payload.push_str(first);
386 } else {
387 res.tokens.push(t);
388 res.payload.push_str(&self.payload[cur..cur + bytes]);
389 cur += bytes;
390 }
391 }
392 _ => break,
393 }
394 }
395 Some(res)
396 }
397
398 #[cfg(feature = "autocomplete")]
399 pub(crate) fn to_completion(&self) -> Option<String> {
400 let mut s = self
401 .first_line()?
402 .render_console(false, Color::Monochrome, usize::MAX);
403 s.truncate(s.trim_end().len());
404 Some(s)
405 }
406}
407
408impl From<&str> for Doc {
409 fn from(value: &str) -> Self {
410 let mut buf = Doc::default();
411 buf.write_str(value, Style::Text);
412 buf
413 }
414}
415
416impl Doc {
417 #[inline(never)]
424 pub(crate) fn token(&mut self, token: Token) {
425 self.tokens.push(token);
426 }
427
428 pub(crate) fn write<T>(&mut self, input: T, style: Style)
429 where
430 T: std::fmt::Display,
431 {
432 use std::fmt::Write;
433 let old_len = self.payload.len();
434 let _ = write!(self.payload, "{}", input);
435 self.set_style(self.payload.len() - old_len, style);
436 }
437
438 #[inline(never)]
439 fn set_style(&mut self, len: usize, style: Style) {
440 match self.tokens.last_mut() {
443 Some(Token::Text {
444 bytes: prev_bytes,
445 style: prev_style,
446 }) if *prev_style == style => {
447 *prev_bytes += len;
448 }
449 _ => {
450 self.tokens.push(Token::Text { bytes: len, style });
451 }
452 }
453 }
454
455 #[inline(never)]
456 pub(crate) fn write_str(&mut self, input: &str, style: Style) {
457 self.payload.push_str(input);
458 self.set_style(input.len(), style);
459 }
460
461 #[inline(never)]
462 pub(crate) fn write_char(&mut self, c: char, style: Style) {
463 self.write_str(c.encode_utf8(&mut [0; 4]), style);
464 }
465}
466
467#[cfg(feature = "docgen")]
468#[derive(Debug, Clone)]
469struct DocSection<'a> {
470 path: Vec<String>,
472 info: &'a Info,
473 meta: &'a Meta,
474}
475
476#[cfg(feature = "docgen")]
477fn extract_sections<'a>(
478 meta: &'a Meta,
479 info: &'a Info,
480 path: &mut Vec<String>,
481 sections: &mut Vec<DocSection<'a>>,
482) {
483 sections.push(DocSection {
484 path: path.clone(),
485 info,
486 meta,
487 });
488 let mut hi = HelpItems::default();
489 hi.append_meta(meta);
490 for item in &hi.items {
491 if let HelpItem::Command {
492 name,
493 short: _,
494 help: _,
495 meta,
496 info,
497 } = item
498 {
499 path.push((*name).to_string());
500 extract_sections(meta, info, path, sections);
501 path.pop();
502 }
503 }
504}