doc_writer/
man.rs

1use crate::DocumentationWriter;
2use std::borrow::Cow;
3use std::io::Write;
4use std::{io, mem};
5
6// see MAN-PAGES(7) and GROFF_MAN(7)
7
8macro_rules! setter {
9    ($(#[$doc:meta])* $var:ident, $with:ident, $set:ident, $typ:ty) => {
10        $(#[$doc])*
11        ///
12        /// Also see
13        #[doc=concat!("[", stringify!($set), "](Self::", stringify!($set), ")")]
14        #[inline]
15        pub fn $with(mut self, $var: $typ) -> Self {
16            self.$var = $var;
17            self
18        }
19
20        $(#[$doc])*
21        ///
22        /// Also see
23        #[doc=concat!("[", stringify!($with), "](Self::", stringify!($with), ")")]
24        #[inline]
25        pub fn set_section(&mut self, $var: $typ) {
26            self.$var = $var;
27        }
28    };
29    ($var:ident, $with:ident, $set:ident) => {
30        setter!($var, $with, $set, Cow<'static, str>);
31    };
32}
33
34/// A [`DocumentationWriter`] that generates man pages.
35///
36/// # Examples
37/// See [`DocumentationWriter`]
38pub struct ManWriter<W: Write> {
39    writer: W,
40
41    section: &'static str,
42    title: Cow<'static, str>,
43    subtitle: Cow<'static, str>,
44    license: Cow<'static, str>,
45
46    wrote_header: bool,
47    see_also: Vec<(String, String)>,
48}
49
50impl<W: Write> ManWriter<W> {
51    /// Create a new instance.
52    pub fn new(w: W) -> Self {
53        Self {
54            writer: w,
55            section: "",
56            title: "".into(),
57            subtitle: "".into(),
58            license: "".into(),
59            wrote_header: false,
60            see_also: vec![],
61        }
62    }
63
64    setter!(
65        /// Set the manual section.
66        ///
67        /// Section | Contents
68        /// --------|---------
69        /// 1 | Executable programs or shell commands
70        /// 2 | System calls (functions provided by the kernel)
71        /// 3 | Library calls (functions within program libraries)
72        /// 4 | Special files (usually found in /dev)
73        /// 5 | File formats and conventions, e.g. /etc/passwd
74        /// 6 | Games
75        /// 7 | Miscellaneous (including macro packages and conventions), e.g. man(7), groff(7)
76        /// 8 | System administration commands (usually only for root)
77        /// 9 | Kernel routines [Non standard]
78        section,
79        with_section,
80        set_section,
81        &'static str
82    );
83}
84
85impl<W: Write> DocumentationWriter for ManWriter<W> {
86    type Error = io::Error;
87
88    fn set_title(&mut self, title: Cow<'static, str>) {
89        self.title = title;
90    }
91
92    fn set_subtitle(&mut self, subtitle: Cow<'static, str>) {
93        self.subtitle = subtitle;
94    }
95
96    fn set_license(&mut self, license: Cow<'static, str>) {
97        self.license = license;
98    }
99
100    fn usage(&mut self, usage: &str) -> Result<(), Self::Error> {
101        self.write_header_once()?;
102        self.write_raw(b".SH \"SYNOPSIS\"\n")?;
103        let mut args_iter = usage.splitn(2, ' ');
104        if let Some(command) = args_iter.next() {
105            self.emphasis(command)?;
106            if let Some(args) = args_iter.next() {
107                self.write_escaped_para(args)?;
108                self.write_raw(b"\n")?;
109            }
110        }
111        Ok(())
112    }
113
114    fn start_description(&mut self) -> Result<(), Self::Error> {
115        self.write_header_once()?;
116        self.write_raw(b".SH \"DESCRIPTION\"\n")
117    }
118
119    fn start_section(&mut self, name: &str) -> Result<(), Self::Error> {
120        self.write_header_once()?;
121        self.write_raw(b".SH ")?;
122        self.write_escaped_str(name.to_uppercase())?;
123        self.write_raw(b"\n")
124    }
125
126    fn plain(&mut self, s: &str) -> Result<(), Self::Error> {
127        self.write_escaped_para(s.trim())?;
128        self.write_raw(b"\n")
129    }
130
131    fn paragraph_break(&mut self) -> Result<(), Self::Error> {
132        self.write_raw(b".P\n")
133    }
134
135    fn emphasis(&mut self, text: &str) -> Result<(), Self::Error> {
136        self.write_raw(b".I ")?;
137        self.write_escaped_para(text)?;
138        self.write_raw(b"\n")
139    }
140
141    fn strong(&mut self, text: &str) -> Result<(), Self::Error> {
142        self.write_raw(b".B ")?;
143        self.write_escaped_para(text)?;
144        self.write_raw(b"\n")
145    }
146
147    fn link(&mut self, text: &str, to: &str) -> Result<(), Self::Error> {
148        self.see_also.push((text.to_owned(), to.to_owned()));
149        if let Some(man) = to.strip_prefix("man:") {
150            if text != "" {
151                self.plain(text)?;
152                self.write_raw(b" (")?;
153            }
154            self.write_raw(b".BR ")?;
155            self.write_escaped_para(man.replace("(", " ("))?;
156            if text != "" {
157                self.write_raw(b" )")?;
158            }
159            self.write_raw(b"\n")?;
160        } else if let Some(mail) = to.strip_prefix("mailto:") {
161            self.write_raw(b".MT ")?;
162            self.write_escaped_para(mail)?;
163            self.write_raw(b"\n")?;
164            if text != "" {
165                self.write_escaped_para(text)?;
166            } else {
167                self.write_escaped_para(mail)?;
168            }
169            self.write_raw(b"\n.ME\n")?;
170        } else {
171            self.write_raw(b".UR ")?;
172            self.write_escaped_para(to)?;
173            self.write_raw(b"\n")?;
174            if text != "" {
175                self.write_escaped_para(text)?;
176            } else {
177                self.write_escaped_para(to)?;
178            }
179            self.write_raw(b"\n.UE\n")?;
180        }
181        Ok(())
182    }
183
184    fn start_options(&mut self) -> Result<(), Self::Error> {
185        self.write_header_once()?;
186        self.write_raw(b".SH \"OPTIONS\"\n")
187    }
188
189    fn option(&mut self, name: &str, default: &str) -> Result<(), Self::Error> {
190        self.write_raw(br#".IP "\fI"#)?;
191        self.write_escaped_para(name)?;
192        if default != "" {
193            self.write_raw(b"=")?;
194            self.write_escaped_para(default)?;
195        }
196        self.write_raw(b"\\fP\" 10\n")
197    }
198
199    fn start_environment(&mut self) -> Result<(), Self::Error> {
200        self.write_header_once()?;
201        self.write_raw(b".SH \"ENVIRONMENT\"\n")
202    }
203
204    fn variable(&mut self, name: &str, default: &str) -> Result<(), Self::Error> {
205        self.option(name, default)
206    }
207
208    fn start_enum(&mut self, name: &str) -> Result<(), Self::Error> {
209        self.write_header_once()?;
210        self.write_raw(b".SH ")?;
211        self.write_escaped_str(name)?;
212        self.write_raw(b"\n")
213    }
214
215    fn variant(&mut self, name: &str) -> Result<(), Self::Error> {
216        self.option(name, "")
217    }
218
219    fn finish(mut self) -> Result<(), Self::Error> {
220        self.write_header_once()?;
221        if !self.see_also.is_empty() {
222            self.write_raw(b".SH \"SEE ALSO\"\n")?;
223            let see_also = mem::take(&mut self.see_also);
224            for also in see_also {
225                self.link(&also.0, &also.1)?;
226            }
227        }
228
229        if self.license != "" {
230            self.write_raw(b"\n.SH \"COPYRIGHT\"")?;
231            let license = mem::take(&mut self.license);
232            let mut lines = license.split('\n').peekable();
233            while let Some(line) = lines.next() {
234                self.write_raw(b"\n")?;
235                self.write_escaped_para(line)?;
236                match lines.peek() {
237                    Some(&"") => {
238                        self.write_raw(b"\n.P")?;
239                        lines.next();
240                    }
241                    Some(_) => self.write_raw(b"\n.br")?,
242                    None => {
243                        lines.next();
244                    }
245                }
246            }
247        }
248        Ok(())
249    }
250}
251
252impl<W: Write> ManWriter<W> {
253    fn write_header_once(&mut self) -> Result<(), io::Error> {
254        if self.wrote_header {
255            return Ok(());
256        }
257        self.wrote_header = true;
258
259        if self.title != "" {
260            let title = mem::take(&mut self.title);
261
262            self.write_raw(b".TH ")?;
263            self.write_escaped_str(title.to_uppercase())?;
264            if self.section != "" {
265                self.write_raw(b" ")?;
266                self.write_escaped_str(self.section)?;
267            } else {
268                self.write_raw(b" \"1\"")?;
269            }
270            self.write_raw(b"\n")?;
271
272            self.write_raw(b".SH \"NAME\"\n")?;
273            self.write_escaped_para(title)?;
274
275            if self.subtitle != "" {
276                self.write_raw(b"\n\\(em ")?;
277                let subtitle = mem::take(&mut self.subtitle);
278                self.write_escaped_para(subtitle)?;
279            }
280            self.write_raw(b"\n")?;
281        }
282
283        Ok(())
284    }
285
286    fn write_raw(&mut self, s: &[u8]) -> Result<(), io::Error> {
287        self.writer.write_all(s)
288    }
289
290    fn write_escaped_para<'a>(&mut self, s: impl Into<Cow<'a, str>>) -> Result<(), io::Error> {
291        let s = s.into();
292        if s.starts_with(&['.', '\''][..]) {
293            self.write_raw(b"\\&")?;
294        }
295        self.writer
296            .write_all(s.replace('"', "\\(dq").replace('\\', "\\e").as_bytes())
297    }
298
299    fn write_escaped_str<'a>(&mut self, s: impl Into<Cow<'a, str>>) -> Result<(), io::Error> {
300        self.write_raw(b"\"")?;
301        self.write_escaped_para(s.into())?;
302        self.write_raw(b"\"")
303    }
304}