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}