sphinx_rustdocgen/
formats.rs

1// sphinxcontrib_rust - Sphinx extension for the Rust programming language
2// Copyright (C) 2024  Munir Contractor
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Module for handling the output formats supported.
18
19use std::fmt::Display;
20use std::str::FromStr;
21
22use serde::Deserialize;
23
24use crate::directives::{Directive, DirectiveVisibility};
25
26/// Generate title decoration string for RST or fence for MD.
27///
28/// Args:
29///     :ch: The character to use.
30///     :len: The length of the decoration required.
31///
32/// Returns:
33///     A string of length ``len`` composed entirely of ``ch``.
34fn generate_decoration(ch: char, len: usize) -> String {
35    let mut decoration = String::with_capacity(len);
36    for _ in 0..len {
37        decoration.push(ch);
38    }
39    decoration
40}
41
42/// Supported formats for the docstrings
43#[derive(Copy, Clone, Debug, Default, Deserialize, Hash, PartialEq, Eq)]
44pub(crate) enum Format {
45    /// Markdown format
46    #[serde(rename(deserialize = "md"))]
47    Md,
48    /// reStructuredText format
49    #[default]
50    #[serde(rename(deserialize = "rst"))]
51    Rst,
52}
53
54impl Format {
55    /// Acceptable text values for Md variant, case-insensitive.
56    const MD_VALUES: [&'static str; 3] = ["md", ".md", "markdown"];
57    /// Acceptable text values for Rst variant, case-insensitive.
58    const RST_VALUES: [&'static str; 3] = ["rst", ".rst", "restructuredtext"];
59
60    /// Returns the extension for the format, without the leading ".".
61    pub fn extension(&self) -> &'static str {
62        match self {
63            Format::Md => Self::MD_VALUES[0],
64            Format::Rst => Self::RST_VALUES[0],
65        }
66    }
67
68    /// Convert the provided text to an inline code representation of the text
69    /// specific to the format.
70    pub(crate) fn make_inline_code<D: Display>(&self, text: D) -> String {
71        match self {
72            Format::Md => format!("`{text}`"),
73            Format::Rst => format!("``{text}``"),
74        }
75    }
76
77    /// Make a format specific document title using the provided title string.
78    pub(crate) fn make_title(&self, title: &str) -> Vec<String> {
79        match self {
80            Format::Md => {
81                vec![format!("# {title}"), String::new()]
82            }
83            Format::Rst => {
84                let decoration = generate_decoration('=', title.len());
85                vec![
86                    decoration.clone(),
87                    title.to_string(),
88                    decoration,
89                    String::new(),
90                ]
91            }
92        }
93    }
94
95    /// Get format specific content for the directive for the output file.
96    ///
97    /// The function assumes that the directive is the top level directive of
98    /// the output file and generates the content accordingly.
99    ///
100    /// Args:
101    ///     :directive: The directive to get the content for, typically a
102    ///         ``Crate`` or ``Module`` directive.
103    ///
104    /// Returns:
105    ///     A vec of strings which are the lines of the document.
106    pub(crate) fn format_directive<T>(
107        &self,
108        directive: T,
109        max_visibility: &DirectiveVisibility,
110    ) -> Vec<String>
111    where
112        T: RstDirective + MdDirective,
113    {
114        match self {
115            Format::Md => {
116                let fence_size = directive.fence_size();
117                directive.get_md_text(fence_size, max_visibility)
118            }
119            Format::Rst => directive.get_rst_text(0, max_visibility),
120        }
121    }
122}
123
124impl FromStr for Format {
125    type Err = String;
126
127    /// Parses the string into an enum value, or panics.
128    ///
129    /// If the string is ``md``, ``.md`` or ``markdown``, the function
130    /// returns ``Md``. If the string is ``rst``, ``.rst`` or
131    /// ``restructuredtext``, the function returns ``Rst``. Comparison is
132    /// case-insensitive.
133    ///
134    /// Args:
135    ///     :value: The value to parse.
136    ///
137    /// Returns:
138    ///     The parsed enum value as the Ok value, or unit type as the Err.
139    fn from_str(value: &str) -> Result<Self, Self::Err> {
140        let value_lower = value.to_lowercase();
141        if Self::RST_VALUES.contains(&&*value_lower) {
142            Ok(Format::Rst)
143        }
144        else if Self::MD_VALUES.contains(&&*value_lower) {
145            Ok(Format::Md)
146        }
147        else {
148            Err(format!("Not a valid format value: {value}"))
149        }
150    }
151}
152
153/// Trait for directives that can be written as RST content
154pub(crate) trait RstDirective {
155    const INDENT: &'static str = "   ";
156
157    /// Generate RST text with the given level of indentation.
158    ///
159    /// Implementations must provide a vec of the lines of the content of the
160    /// item and all its members.
161    ///
162    /// Args:
163    ///     :level: The level of indentation for the content. Use the
164    ///         ``make_indent`` and ``make_content_indent`` functions to get
165    ///         the actual indentation string.
166    ///     :max_visibility: Include only items with visibility up to the
167    ///         defined level.
168    ///
169    /// Returns:
170    ///     The RST text for the documentation of the item and its members.
171    fn get_rst_text(self, level: usize, max_visibility: &DirectiveVisibility) -> Vec<String>;
172
173    /// Make a string for indenting the directive.
174    ///
175    /// Args:
176    ///     :level: The level of the indentation.
177    ///
178    /// Returns:
179    ///     A string that is ``Self::INDENT`` repeated ``level`` times.
180    fn make_indent(level: usize) -> String {
181        let mut indent = String::with_capacity(Self::INDENT.len() * level);
182        for _ in 0..level {
183            indent += Self::INDENT;
184        }
185        indent
186    }
187
188    /// Make a string for indenting the directive's content and options
189    ///
190    /// Args:
191    ///     :level: The level of the indentation.
192    ///
193    /// Returns:
194    ///     A string that is ``Self::INDENT`` repeated ``level + 1`` times.
195    fn make_content_indent(level: usize) -> String {
196        Self::make_indent(level + 1)
197    }
198
199    /// Make the RST directive header from the directive, name and options.
200    ///
201    /// Args:
202    ///     :directive: The RST directive to make the header for.
203    ///     :name: The name of the directive.
204    ///     :options: The directive options to add.
205    ///     :level: The indentation level of the directive.
206    ///
207    /// Returns:
208    ///     A Vec of the directive's header lines.
209    fn make_rst_header<O: RstOption, D: Display, E: Display>(
210        directive: D,
211        name: E,
212        options: &[O],
213        level: usize,
214    ) -> Vec<String> {
215        let indent = &Self::make_indent(level);
216        let option_indent = &Self::make_indent(level + 1);
217        let mut header = Vec::with_capacity(3 + options.len());
218        header.push(String::new());
219        header.push(
220            format!("{indent}.. rust:{directive}:: {name}")
221                .trim_end()
222                .to_string(),
223        );
224        options
225            .iter()
226            .filter_map(|o| o.get_rst_text(option_indent))
227            .for_each(|t| header.push(t));
228        header.push(String::new());
229        header
230    }
231
232    /// Make a ``toctree`` directive for RST documents.
233    ///
234    /// Args:
235    ///     :indent: The indentation for the directive.
236    ///     :caption: The caption for the ``toctree``.
237    ///     :maxdepth: The desired ``maxdepth`` of the ``toctree``. If None,
238    ///         the ``:maxdepth:`` option will not be set.
239    ///     :tree: The ``toctree`` entries.
240    fn make_rst_toctree<I: Display, T: Iterator<Item = I>>(
241        indent: &str,
242        caption: &str,
243        max_depth: Option<u8>,
244        tree: T,
245    ) -> Vec<String> {
246        let tree: Vec<I> = tree.collect();
247        if tree.is_empty() {
248            return Vec::new();
249        }
250
251        let mut toc_tree = vec![
252            String::new(),
253            format!("{indent}.. rubric:: {caption}"),
254            format!("{indent}.. toctree::"),
255        ];
256        if let Some(md) = max_depth {
257            toc_tree.push(format!("{indent}{}:maxdepth: {md}", Self::INDENT));
258        }
259        toc_tree.push(String::new());
260
261        for item in tree {
262            toc_tree.push(format!("{indent}{}{item}", Self::INDENT));
263        }
264        toc_tree.push(String::new());
265        toc_tree
266    }
267
268    /// Make section in an RST document with the given title and items.
269    ///
270    /// Args:
271    ///     :section: The title of the section.
272    ///     :level: The indentation level of the section.
273    ///     :items: The items to include in the section.
274    ///     :max_visibility: The max visibility of the items to include.
275    ///
276    /// Returns:
277    ///     The RST text for the section.
278    fn make_rst_section(
279        section: &str,
280        level: usize,
281        items: Vec<Directive>,
282        max_visibility: &DirectiveVisibility,
283    ) -> Vec<String> {
284        let indent = Self::make_content_indent(level);
285        let mut section = vec![
286            String::new(),
287            format!("{indent}.. rubric:: {section}"),
288            String::new(),
289        ];
290        for item in items {
291            section.extend(item.get_rst_text(level + 1, max_visibility))
292        }
293        // If nothing was added to the section, return empty vec.
294        if section.len() > 3 {
295            section
296        }
297        else {
298            Vec::new()
299        }
300    }
301
302    /// Make an RST list of items.
303    ///
304    /// Args:
305    ///     :indent: The indentation for the list items.
306    ///     :title: The title for the list.
307    ///     :items: A vec of item name and content tuples.
308    ///
309    /// Returns:
310    ///     Lines of RST text for the list, with the title.
311    fn make_rst_list(indent: &str, title: &str, items: &[(String, Vec<String>)]) -> Vec<String> {
312        if items.is_empty() {
313            return vec![];
314        }
315
316        let mut text = vec![format!("{indent}.. rubric:: {title}"), String::new()];
317        for (item, content) in items {
318            text.push(format!("{indent}* :rust:any:`{item}`"));
319            text.extend(content.iter().map(|l| format!("{indent}  {l}")));
320        }
321        text
322    }
323}
324
325/// Trait for directives that can be written as MD content
326pub(crate) trait MdDirective {
327    const DEFAULT_FENCE_SIZE: usize = 4;
328
329    /// Generate MD text with the given fence size.
330    ///
331    /// Implementations must provide a vec of the lines of the content of the
332    /// item and all its members.
333    ///
334    /// Args:
335    ///     :fence_size: The size of the fence for the directive. Use the
336    ///         ``make_fence`` function to get the actual fence string.
337    ///     :max_visibility: Include only items with visibility up to the
338    ///         defined level.
339    ///
340    /// Returns:
341    ///     The MD text for the documentation of the item and its members.
342    fn get_md_text(self, fence_size: usize, max_visibility: &DirectiveVisibility) -> Vec<String>;
343
344    /// Make a string for the fences for the directive.
345    ///
346    /// Args:
347    ///     :fence_size: The size of the fence, must be at least 3.
348    ///
349    /// Returns:
350    ///     A string of colons of length ``fence_size``.
351    ///
352    /// Panics:
353    ///     If the ``fence_size`` is less than 3.
354    fn make_fence(fence_size: usize) -> String {
355        if fence_size < 3 {
356            panic!("Invalid fence size {fence_size}. Must be >= 3");
357        }
358        generate_decoration(':', fence_size)
359    }
360
361    /// Calculate the fence size required for the item.
362    ///
363    /// The ``items`` are the members of the current item. So, for
364    /// a struct, these will be the list of its fields, for an enum,
365    /// the variants, for a module, the items defined in it, etc.
366    ///
367    /// The fence size for the item is 1 + the max fence size of all
368    /// its members. If it has no members, the fence size is the default fence
369    /// size. So, the returned value is the minimum fence size required to
370    /// properly document the item and its members in Markdown.
371    ///
372    /// Args:
373    ///     :items: Items which are members of the current item.
374    ///
375    /// Returns:
376    ///     The minimum fence size required to document the item and all its
377    ///     nested items.
378    fn calc_fence_size(items: &[Directive]) -> usize {
379        match items.iter().map(Directive::fence_size).max() {
380            Some(s) => s + 1,
381            None => Self::DEFAULT_FENCE_SIZE,
382        }
383    }
384
385    /// Make the MD directive header from the directive, name and options.
386    ///
387    /// Args:
388    ///     :directive: The MD directive to make the header for.
389    ///     :name: The name of the directive.
390    ///     :options: The directive options to add.
391    ///     :fence: The fence to use for the directive.
392    ///
393    /// Returns:
394    ///     A Vec of the directive's header lines.
395    fn make_md_header<O: MdOption, D: Display, E: Display>(
396        directive: D,
397        name: E,
398        options: &[O],
399        fence: &str,
400    ) -> Vec<String> {
401        let mut header = Vec::with_capacity(2 + options.len());
402        header.push(
403            format!("{fence}{{rust:{directive}}} {name}")
404                .trim_end()
405                .to_string(),
406        );
407        options
408            .iter()
409            .filter_map(|o| o.get_md_text())
410            .for_each(|t| header.push(t));
411        header.push(String::new());
412        header
413    }
414
415    /// Make a ``toctree`` directive for MD documents.
416    ///
417    /// Args:
418    ///     :fence_size: The fence size for the directive.
419    ///     :caption: The caption for the ``toctree``.
420    ///     :maxdepth: The desired ``maxdepth`` of the ``toctree``. If None,
421    ///         the ``:maxdepth:`` option will not be set.
422    ///     :tree: The ``toctree`` entries.
423    fn make_md_toctree<I: Display, T: Iterator<Item = I>>(
424        fence_size: usize,
425        caption: &str,
426        max_depth: Option<u8>,
427        tree: T,
428    ) -> Vec<String> {
429        let tree: Vec<I> = tree.collect();
430        if tree.is_empty() {
431            return Vec::new();
432        }
433
434        let fence = Self::make_fence(fence_size);
435        let mut toc_tree = vec![
436            String::new(),
437            format!("{fence}{{rubric}} {caption}"),
438            fence.clone(),
439            format!("{fence}{{toctree}}"),
440        ];
441        if let Some(md) = max_depth {
442            toc_tree.push(format!(":maxdepth: {md}"));
443        }
444        toc_tree.push(String::new());
445        for item in tree {
446            toc_tree.push(item.to_string());
447        }
448        toc_tree.push(fence);
449        toc_tree
450    }
451
452    /// Make section in an MD document with the given title and items.
453    ///
454    /// Args:
455    ///     :section: The title of the section.
456    ///     :fence_size: The fence size of the section.
457    ///     :items: The items to include in the section.
458    ///     :max_visibility: The max visibility of the items to include.
459    ///
460    /// Returns:
461    ///     The MD text for the section.
462    fn make_md_section(
463        section: &str,
464        fence_size: usize,
465        items: Vec<Directive>,
466        max_visibility: &DirectiveVisibility,
467    ) -> Vec<String> {
468        let fence = Self::make_fence(3);
469        let mut section = vec![
470            String::new(),
471            format!("{fence}{{rubric}} {section}"),
472            fence,
473            String::new(),
474        ];
475        for item in items {
476            section.extend(item.get_md_text(fence_size - 1, max_visibility))
477        }
478        // If nothing was added to the section, return empty vec.
479        if section.len() > 4 {
480            section
481        }
482        else {
483            Vec::new()
484        }
485    }
486
487    /// Make an MD list of items.
488    ///
489    /// Args:
490    ///     :fence_size: The fence size for the list title.
491    ///     :title: The title for the list.
492    ///     :items: A vec of item name and content tuples.
493    ///
494    /// Returns:
495    ///     Lines of MD text for the list, with the title.
496    fn make_md_list(
497        fence_size: usize,
498        title: &str,
499        items: &[(String, Vec<String>)],
500    ) -> Vec<String> {
501        if items.is_empty() {
502            return vec![];
503        }
504
505        let fence = Self::make_fence(fence_size);
506        let mut text = vec![format!("{fence}{{rubric}} {title}"), fence, String::new()];
507        for (item, content) in items {
508            text.push(format!(
509                "* {{rust:any}}`{item}`{}",
510                if content.is_empty() {
511                    ""
512                }
513                else {
514                    "  "
515                }
516            ));
517            text.extend(content.iter().map(|l| format!("  {l}")));
518        }
519        text
520    }
521
522    /// Return the fence size required for documenting the item.
523    ///
524    /// The default implementation returns ``4``, which allows for members
525    /// with no items to create sections within the docstrings, that do not
526    /// show up in the ``toctree``.
527    ///
528    /// Implementations may use
529    /// :rust:fn:`MdDirective::calc_fence_size`
530    /// to override this, when there are nested items present.
531    fn fence_size(&self) -> usize {
532        Self::DEFAULT_FENCE_SIZE
533    }
534}
535
536/// Trait for RST directive options.
537pub(crate) trait RstOption {
538    /// Return the RST text for the option.
539    fn get_rst_text(&self, indent: &str) -> Option<String>;
540}
541
542/// Trait for MD directive options
543pub(crate) trait MdOption {
544    /// Return the MD text for the option.
545    fn get_md_text(&self) -> Option<String>;
546}
547
548/// Trait for anything that can be converted to RST directive content.
549///
550/// This is implemented for all ``IntoIterator<Item = String>``, effectively
551/// allowing ``Vec<String>`` to be converted to RST content lines.
552pub(crate) trait RstContent {
553    fn get_rst_text(self, indent: &str) -> Vec<String>;
554}
555
556impl<T> RstContent for T
557where
558    T: IntoIterator<Item = String>,
559{
560    fn get_rst_text(self, indent: &str) -> Vec<String> {
561        self.into_iter().map(|s| format!("{indent}{s}")).collect()
562    }
563}
564
565/// Trait for anything that can be converted to MD directive content.
566///
567/// This is implemented for all ``IntoIterator<Item = String>``, effectively
568/// allowing ``Vec<String>`` to be converted to MD content lines.
569pub(crate) trait MdContent {
570    fn get_md_text(self) -> Vec<String>;
571}
572
573impl<T> MdContent for T
574where
575    T: IntoIterator<Item = String>,
576{
577    fn get_md_text(self) -> Vec<String> {
578        let mut text = vec![String::from("  :::")];
579        text.extend(self.into_iter().map(|s| format!("  {s}")));
580        text.push(String::from("  :::"));
581        text
582    }
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    #[test]
590    fn test_decoration() {
591        assert_eq!(generate_decoration('=', 0), "");
592        assert_eq!(generate_decoration('=', 1), "=");
593        assert_eq!(generate_decoration('=', 5), "=====");
594    }
595
596    #[test]
597    fn test_format() {
598        let rst = Format::Rst;
599        assert_eq!(rst.extension(), "rst");
600        assert_eq!(rst.make_title("foo"), vec!["===", "foo", "===", ""]);
601        assert_eq!(
602            rst.make_title(&rst.make_inline_code("foo")),
603            vec!["=======", "``foo``", "=======", ""]
604        );
605        assert_eq!(Format::from_str("rst").unwrap(), rst);
606
607        let md = Format::Md;
608        assert_eq!(md.extension(), "md");
609        assert_eq!(md.make_title("foo"), vec!["# foo", ""]);
610        assert_eq!(
611            md.make_title(&md.make_inline_code("foo")),
612            vec!["# `foo`", ""]
613        );
614        assert_eq!(Format::from_str("md").unwrap(), md);
615
616        assert!(Format::from_str("foo").is_err());
617    }
618
619    #[test]
620    fn test_content_traits() {
621        let text: Vec<String> = ["line 1", "line 2", "line 3"]
622            .iter()
623            .map(|&s| s.to_string())
624            .collect();
625        let expected = vec!["  :::", "  line 1", "  line 2", "  line 3", "  :::"];
626        assert_eq!(text.clone().get_rst_text("  "), &expected[1..4]);
627        assert_eq!(text.clone().get_md_text(), expected);
628    }
629}