user_doc_doc_data/
lib.rs

1//! Metadata regarding user-facing documentation
2use once_cell::sync::Lazy;
3use serde::{Deserialize, Serialize};
4use std::{
5  collections::BTreeMap,
6  fs::{self, File},
7  io::BufReader,
8  path::PathBuf,
9  sync::RwLock,
10};
11use strum::{AsRefStr, EnumIter};
12use syn::{
13  parse_str, Attribute, AttributeArgs, Error, Lit, LitInt, LitStr, Meta, MetaList, MetaNameValue,
14  NestedMeta, Path, Result,
15};
16
17/// Line ending
18const END: &str = "  \n";
19/// Output directory for persisting data between compilation-time and run-time
20pub static OUTPUT_DIR_PATH: &str = "user_doc";
21/// Output file name for persisting data between compilation-time and run-time
22static OUTPUT_FILE_NAME: &str = "user_doc.json";
23/// Custom output file path for persisting data
24pub static CUSTOM_OUTPUT_FILE_NAME: Lazy<RwLock<Option<String>>> = Lazy::new(|| RwLock::new(None));
25/// The repository of all active docs
26pub static DOCS: Lazy<RwLock<DocDict>> =
27  Lazy::new(|| RwLock::new(DocDict(BTreeMap::new(), Vec::new())));
28/// How much text to show in previews
29pub const PREVIEW_TEXT_LENGTH: usize = 16;
30
31#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
32/// When expanding to directories, this determines the naming scheme
33pub enum DirectoryNamingScheme {
34  /// Use the deepest-depth chapter name
35  ChapterName,
36  /// Use the deepest-depth chapter number
37  ChapterNumber,
38}
39
40#[derive(Clone, Serialize, Deserialize, Debug, Eq, Ord, PartialEq, PartialOrd)]
41/// A documentation or a Docdict
42/// Each documentable has a name string
43pub enum Documentable {
44  Doc(String, String),
45  BoxedDocDict(String, Box<DocDict>),
46}
47
48impl core::fmt::Display for Documentable {
49  fn fmt(
50    &self,
51    f: &'_ mut core::fmt::Formatter,
52  ) -> core::fmt::Result {
53    match self {
54      Self::Doc(ref name, ref text) => {
55        write!(
56          f,
57          "Documentable::Doc(`{}`: {})",
58          name,
59          if text.len() > PREVIEW_TEXT_LENGTH {
60            let mut text = text.to_string();
61            text.truncate(PREVIEW_TEXT_LENGTH - 3);
62            format!("{}...", text)
63          } else {
64            text.to_string()
65          }
66        )
67      }
68      Self::BoxedDocDict(ref name, ref boxed_doc_dict) => {
69        write!(
70          f,
71          "Documentable::BoxedDocDict(`{}`: {} subchapters)",
72          name,
73          boxed_doc_dict.len()
74        )
75      }
76    }
77  }
78}
79
80#[allow(clippy::enum_variant_names)]
81#[derive(AsRefStr, Clone, EnumIter, Debug, Eq, Ord, PartialOrd, PartialEq)]
82#[strum(serialize_all = "snake_case")]
83/// Attribute helpers
84///
85/// When written in snake case (eg. ChapterNum -> chapter_num),
86/// the _variants_ of this enum correspond to the
87///
88/// - _helper attributes_ supported by the `user_doc_item` derive macro:
89/// ```ignore
90/// #[derive(user_doc_item)]
91/// // This line will NOT be in the produced documentation.
92/// struct SomeStruct {
93///   #[chapter_name("A chapter")]
94///   #[chapter_num(333)]
95///   // This line will be in the produced documentation.
96///   some_field: u8
97/// }
98/// ```  
99/// - _arguments_ supported by the `user_doc_fn` attribute macro:
100/// ```ignore
101/// #[user_doc_fn(chapter_name("A chapter"), chapter_num(333))]
102/// // This line will be in the produced documentation.
103/// fn some_fn() -> () {}
104/// ```  
105///
106/// The preceding examples both produce the same user-facing documentation.
107/// If both examples occurred in the same project, however, one would
108/// overwrite the other.
109pub enum HelperAttr {
110  /// Number of this chapter
111  ChapterNum(usize),
112  /// Number-path up to and including this chapter
113  ChapterNumSlug(Vec<usize>),
114  /// Name of this chapter
115  ChapterName(String),
116  /// Name-path up to and including this chapter
117  ChapterNameSlug(Vec<String>),
118  /// A blurb to add to the page for the chapter
119  ChapterBlurb(String),
120}
121
122impl From<&HelperAttr> for Path {
123  fn from(h: &HelperAttr) -> Self {
124    parse_str::<Self>(h.as_ref()).expect("Must create path from HelperAttr")
125  }
126}
127impl HelperAttr {
128  /// Instantiate from an AttributeArgs (Vec<NestedMeta>)
129  #[allow(clippy::ptr_arg)]
130  pub fn from_attribute_args(a: &AttributeArgs) -> Result<Vec<Self>> {
131    let mut selves = Vec::with_capacity(a.len());
132    for nested_meta in a {
133      match nested_meta {
134        NestedMeta::Meta(ref meta) => match meta {
135          Meta::Path(ref path) => {
136            return Err(Error::new_spanned(path, "unsupported attribute subpath"))
137          }
138          Meta::List(MetaList {
139            ref path,
140            ref nested,
141            ..
142          }) => {
143            if path.is_ident(Self::ChapterNameSlug(vec![]).as_ref()) {
144              let mut slugs = vec![];
145              for nested_meta in nested.iter() {
146                match nested_meta {
147                  NestedMeta::Lit(Lit::Str(ref lit_str)) => {
148                    slugs.push(lit_str.value());
149                  }
150                  _ => {
151                    return Err(Error::new_spanned(
152                      nested_meta,
153                      "Unsupported nested meta attribute ",
154                    ))
155                  }
156                }
157              }
158              selves.push(Self::ChapterNameSlug(slugs));
159            } else if path.is_ident(Self::ChapterNumSlug(Vec::new()).as_ref()) {
160              let mut slugs = vec![];
161              for nested_meta in nested.iter() {
162                match nested_meta {
163                  NestedMeta::Lit(Lit::Int(ref lit_int)) => {
164                    slugs.push(lit_int.base10_parse()?);
165                  }
166                  _ => {
167                    return Err(Error::new_spanned(
168                      nested_meta,
169                      "Unsupported nested meta attribute ",
170                    ))
171                  }
172                }
173              }
174              selves.push(Self::ChapterNumSlug(slugs));
175            } else {
176              return Err(Error::new_spanned(path, "Unsupported meta list path"));
177            }
178          }
179          Meta::NameValue(MetaNameValue {
180            ref path, ref lit, ..
181          }) => {
182            if path.is_ident(Self::ChapterName(String::new()).as_ref()) {
183              match lit {
184                Lit::Str(ref lit_str) => {
185                  selves.push(Self::ChapterName(lit_str.value()));
186                }
187                bad_lit => {
188                  return Err(Error::new_spanned(
189                    bad_lit,
190                    "Unsupported chapter name literal",
191                  ));
192                }
193              }
194            } else if path.is_ident(Self::ChapterNum(0usize).as_ref()) {
195              match lit {
196                Lit::Int(ref lit_int) => {
197                  selves.push(Self::ChapterNum(lit_int.base10_parse()?));
198                }
199                bad_lit => {
200                  return Err(Error::new_spanned(
201                    bad_lit,
202                    "Unsupported chapter number literal",
203                  ));
204                }
205              }
206            } else if path.is_ident(Self::ChapterBlurb(String::new()).as_ref()) {
207              match lit {
208                Lit::Str(ref lit_str) => {
209                  selves.push(Self::ChapterBlurb(lit_str.value()));
210                }
211                bad_lit => {
212                  return Err(Error::new_spanned(
213                    bad_lit,
214                    "Unsupported chapter blurb literal",
215                  ));
216                }
217              }
218            } else {
219              return Err(Error::new_spanned(
220                path,
221                "unrecognized helper attribute inner",
222              ));
223            }
224          }
225        },
226        _ => {
227          return Err(Error::new_spanned(
228            nested_meta,
229            "unrecognized helper attribute",
230          ));
231        }
232      }
233    }
234    Ok(selves)
235  }
236
237  /// Instantiate from an Attribute if said Attribute represents a valid HelperAttr
238  pub fn from_attribute(a: &Attribute) -> Result<Self> {
239    let Attribute {
240      path, tokens: _, ..
241    } = a;
242    if path.is_ident(&Self::ChapterNum(0).as_ref()) {
243      let chapter_num_lit_int = a.parse_args::<LitInt>()?;
244      Ok(Self::ChapterNum(chapter_num_lit_int.base10_parse()?))
245    } else if path.is_ident(&Self::ChapterName(String::new()).as_ref()) {
246      let chapter_name_lit_str = a.parse_args::<LitStr>()?;
247      Ok(Self::ChapterName(chapter_name_lit_str.value()))
248    } else if path.is_ident(&Self::ChapterBlurb(String::new()).as_ref()) {
249      let chapter_name_blurb_str = a.parse_args::<LitStr>()?;
250      Ok(Self::ChapterBlurb(chapter_name_blurb_str.value()))
251    } else if path.is_ident(&Self::ChapterNameSlug(Vec::new()).as_ref()) {
252      let meta = a.parse_meta()?;
253      match meta {
254        Meta::List(MetaList { nested, .. }) => {
255          let segments: Vec<String> = nested
256            .iter()
257            .filter_map(|nested_meta| match nested_meta {
258              NestedMeta::Lit(Lit::Str(lit_str)) => Some(lit_str.value()),
259              _ => None,
260            })
261            .collect();
262          Ok(Self::ChapterNameSlug(segments))
263        }
264        bad => Err(Error::new_spanned(
265          bad,
266          "unrecognized attribute payload for chapter_name_slug",
267        )),
268      }
269    } else if path.is_ident(&Self::ChapterNumSlug(Vec::new()).as_ref()) {
270      let meta = a.parse_meta()?;
271      match meta {
272        Meta::List(MetaList { nested, .. }) => {
273          let mut segments_results: Vec<Result<usize>> = nested
274            .iter()
275            .filter_map(|nested_meta| match nested_meta {
276              NestedMeta::Lit(Lit::Int(lit_int)) => Some(lit_int.base10_parse()),
277              _ => None,
278            })
279            .collect();
280          let mut segments: Vec<usize> = Vec::with_capacity(segments_results.len());
281          for segment_result in segments_results.drain(0..) {
282            segments.push(segment_result?);
283          }
284          Ok(Self::ChapterNumSlug(segments))
285        }
286        bad => Err(Error::new_spanned(
287          bad,
288          "unrecognized attribute payload for chapter_name_slug",
289        )),
290      }
291    } else {
292      Err(Error::new_spanned(path, "unrecognized helper attribute"))
293    }
294  }
295}
296
297/// The level of a markdown header
298pub type HeaderLevel = u8;
299/// Constrain the level of a mark header
300fn constrain_header_level(header_level: HeaderLevel) -> HeaderLevel {
301  header_level.clamp(0, 6)
302}
303/// Make a markdown header
304fn make_header(header_level: HeaderLevel) -> String {
305  let mut s = String::new();
306  for _i in 0..header_level {
307    s.push('#');
308  }
309  s
310}
311/// Make a markdown  named header
312fn make_named_header(
313  header_level: HeaderLevel,
314  header_name: &str,
315) -> String {
316  format!("{} {}", make_header(header_level), header_name)
317}
318/// Make a markdown separator
319fn make_separator() -> String {
320  "---".to_string()
321}
322fn make_chapter_num_string(chap_nums_vec: &[usize]) -> String {
323  let mut s = String::new();
324  for n in chap_nums_vec.iter() {
325    s = format!("{}.{}", s, n);
326  }
327  s
328}
329impl core::cmp::PartialEq<&str> for Documentable {
330  fn eq(
331    &self,
332    rhs: &&str,
333  ) -> bool {
334    match self {
335      Self::Doc(_, contents) => contents.eq(rhs),
336      _ => false,
337    }
338  }
339}
340impl Documentable {
341  /// Get a mutable reference to the inner chapter (BoxedDocDict) contained if any
342  pub fn get_inner_boxed_doc_dict_mut_ref(&mut self) -> Option<&mut Box<DocDict>> {
343    match self {
344      Self::Doc(_, _) => None,
345      Self::BoxedDocDict(_, ref mut boxed_doc_dict) => Some(boxed_doc_dict),
346    }
347  }
348
349  /// Get this item's name
350  pub fn name(&self) -> String {
351    match self {
352      Self::Doc(ref name, ..) => name.clone(),
353      Self::BoxedDocDict(ref name, ..) => name.clone(),
354    }
355  }
356
357  /// Coerce this documentation to a string of markdown
358  pub fn to_string(
359    &self,
360    header_level: HeaderLevel,
361    chapter_nums_opt: Option<Vec<usize>>,
362  ) -> String {
363    let chapter_nums = chapter_nums_opt.unwrap_or_default();
364    let header_level = constrain_header_level(header_level);
365    let mut s = String::new();
366    match self {
367      Documentable::Doc(ref name, ref doc_text) => {
368        s.push_str(&format!(
369          "{}{}{}",
370          make_named_header(header_level, name),
371          END,
372          doc_text
373        ));
374      }
375      Documentable::BoxedDocDict(ref name, ref boxed_doc_dict) => {
376        let count = boxed_doc_dict.len();
377        for (subchapter_num, (subchapter_name, documentable)) in boxed_doc_dict.iter() {
378          let mut chapter_nums = chapter_nums.clone();
379          chapter_nums.push(*subchapter_num);
380          let sep_string = if *subchapter_num < count - 1 {
381            make_separator()
382          } else {
383            String::new()
384          };
385          s.push_str(&format!(
386            "{}{}
387            {}{}
388            {}{}
389            {}{}
390            {}{}",
391            make_named_header(header_level, name),
392            END,
393            make_chapter_num_string(&chapter_nums),
394            END,
395            make_named_header(header_level + 1, subchapter_name),
396            END,
397            documentable.to_string(header_level + 1, Some(chapter_nums),),
398            END,
399            sep_string,
400            END
401          ))
402        }
403      }
404    }
405
406    s
407  }
408}
409
410/// Entries in the DocDict take this form
411pub type DocDictEntryValueType = (String, Documentable);
412/// A tree-dictionary of docs
413pub type DocDictTree = BTreeMap<usize, DocDictEntryValueType>;
414#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, Ord, PartialEq, PartialOrd)]
415/// A (possibly-nested) dictionary of docs
416pub struct DocDict(
417  /// The dictionary tree
418  pub DocDictTree,
419  /// Backlinks and blurbs
420  pub Vec<(Vec<usize>, String)>,
421);
422impl DocDict {
423  /// Add an entry to a documentation dictionary
424  pub fn add_entry(
425    &mut self,
426    documentable: Documentable,
427    name_opt: Option<String>,
428    number_opt: Option<usize>,
429    overwrite_opt: Option<bool>,
430  ) -> anyhow::Result<()> {
431    let number = number_opt.unwrap_or_default();
432    let name = name_opt.unwrap_or_else(|| documentable.name()); // take name from documentable
433    let (already_has_number, already_has_name) = self.iter().fold(
434      (false, false),
435      |(has_number, has_name), (num_i, (nam_i, _))| {
436        (has_number || num_i == &number, has_name || nam_i == &name)
437      },
438    );
439    // std::println!("already_has_number {}: {}, already_has_name `{}` {}", number, already_has_number, name, already_has_name);
440    match (already_has_number, already_has_name) {
441      (false, false) => {
442        self.insert(number, (name, documentable));
443        Ok(())
444      }
445      (false, true) => {
446        // Duplicate names are acceptable
447        self.insert(number, (name, documentable));
448        Ok(())
449      }
450      (true, false) => {
451        // Duplicate numbers are not acceptable
452        let number = self.find_next_entry_number();
453        self.insert(number, (name, documentable));
454        Ok(())
455      }
456      (true, true) => {
457        let overwrite_falsy_message = format!(
458          "Attempted to insert duplicate entry for (chapter, title) ({} {}). 
459          \nTry setting overwrite_opt param to Some(true)",
460          number, name
461        );
462        if let Some(must_overwrite) = overwrite_opt {
463          if must_overwrite {
464            self.insert(number, (name, documentable));
465            Ok(())
466          } else {
467            anyhow::bail!(overwrite_falsy_message)
468          }
469        } else {
470          // Duplicate numbers are not acceptable
471          anyhow::bail!(overwrite_falsy_message);
472        }
473      }
474    }
475  }
476
477  /// Add the path specified to a documentation dictionary, filling with empty subtrees to get there.
478  /// Return Ok(()) on success.
479  /// - If the path exists, fail unless `overwrite_opt` contains `true`
480  pub fn add_path(
481    &mut self,
482    chapter_blurb_opt: &Option<String>,
483    name_opt: &Option<String>,
484    documentable_opt: Option<Documentable>,
485    overwrite_opt: Option<bool>,
486    path_names: &[String],
487    path_numbers: &[usize],
488  ) -> anyhow::Result<()> {
489    use anyhow::Context;
490    // let _name = name_opt.clone().unwrap_or_default();
491    let mut i = 0;
492    let mut subdict = self;
493    let mut paths_found = vec![false; path_numbers.len()];
494    while i < path_numbers.len() {
495      let chapter_num = path_numbers
496        .get(i)
497        .cloned()
498        .unwrap_or_else(|| subdict.find_next_entry_number());
499      let chapter_name = path_names
500        .get(i)
501        .or(name_opt.as_ref())
502        .cloned()
503        .unwrap_or_default();
504      let empty_chapter = Documentable::BoxedDocDict(
505        chapter_name.clone(),
506        Box::new(DocDict(BTreeMap::new(), Vec::new())),
507      );
508      if !subdict
509        .iter()
510        .any(|(num, (_name, _contents))| num == &chapter_num)
511      {
512        let documentable = if i == path_numbers.len() - 1 {
513          // this is the target point of the path, where the optional documentable should go
514          documentable_opt.clone().unwrap_or(empty_chapter)
515        } else {
516          // the default empty chapter
517          empty_chapter
518        };
519        // add the new chapter
520        subdict.add_entry(
521          documentable,
522          Some(chapter_name.clone()),
523          Some(chapter_num),
524          overwrite_opt,
525        )?;
526        // add the backlink and add - chapter blurb to the parent chapter text
527        if let Some(ref chapter_blurb) = chapter_blurb_opt {
528          let back_link = path_numbers.to_vec();
529          subdict.1.push((back_link, chapter_blurb.to_string()));
530        }
531        // std::println!("updated subdict {:#?}", subdict );
532      } else {
533        // just mark that this chapter already existed
534        paths_found[i] = true;
535      }
536      if i < path_numbers.len() - 1 {
537        // Get the inner sub dictionary
538        subdict = subdict
539          .get_mut(&chapter_num)
540          .with_context(|| {
541            format!(
542              "Must get new chapter ({} (name: {}))",
543              chapter_num, chapter_name,
544            )
545          })?
546          .1
547          .get_inner_boxed_doc_dict_mut_ref()
548          .with_context(|| {
549            format!(
550              "Must get newly inserted chapter ({} (name: {}))",
551              chapter_num, chapter_name,
552            )
553          })?;
554      }
555      // Increment depth
556      i += 1;
557    }
558    Ok(())
559  }
560
561  /// Produce a depth-first, _immutable_ iterator over the entries.
562  ///
563  /// - The iterator item includes a slug of chapters, the current entry node,
564  /// and the number of sub entries for the current  node
565  /// The iterator will produce entries for the chapters AS WELL AS entries for the subchapters of
566  /// those chapters.
567  pub fn deep_iter(
568    &self,
569    start_slug_opt: Option<Vec<usize>>,
570  ) -> std::collections::vec_deque::IntoIter<(Vec<usize>, &DocDictEntryValueType, usize)> {
571    use std::collections::VecDeque;
572    let mut vv: VecDeque<(Vec<usize>, &DocDictEntryValueType, usize)> = VecDeque::new();
573    let start_slug = start_slug_opt.unwrap_or_default();
574    if !self.0.is_empty() {
575      // go through each root item and check what it contains
576      for (k, entry) in self.0.iter() {
577        // record a new slug for this position
578        let mut iter_slug = start_slug.clone();
579        iter_slug.push(*k);
580        if let Documentable::BoxedDocDict(_, dd) = &entry.1 {
581          vv.push_back((iter_slug.clone(), entry, (*dd).len()));
582          // add the descent items for this node
583          vv.extend(dd.deep_iter(Some(iter_slug)))
584        } else {
585          vv.push_back((iter_slug, entry, 0usize));
586        }
587      }
588    }
589    vv.into_iter()
590  }
591
592  /// Expand this doc dict into directories at the given _directory_ path
593  pub fn expand_into_mdbook_dirs_at_path(
594    &self,
595    naming_scheme: DirectoryNamingScheme,
596    root_path: &str,
597  ) -> anyhow::Result<()> {
598    use anyhow::Context;
599    const README_NAME: &str = "README";
600    let dir_path: PathBuf = root_path.into();
601    if !dir_path.is_dir() {
602      fs::create_dir_all(dir_path.clone())
603        .with_context(|| format!("Must create root path {:?}", dir_path))?;
604    }
605    let one_indentation = "  ";
606    let indent_for_depth = |depth: usize| -> String {
607      let v = vec![one_indentation; depth];
608      v.join("")
609    };
610    let mut slugs_to_paths: BTreeMap<Vec<usize>, PathBuf> = BTreeMap::new();
611    let mut summary_md_contents = String::from("# Summary");
612    if !self.1.is_empty() {
613      for chapter_summary_line in self.1.iter() {
614        summary_md_contents.push_str(&format!("\n{}  ", chapter_summary_line.1));
615      }
616    }
617    for (iter_slug, (name, documentable), _sub_entries_len) in self.deep_iter(None) {
618      let depth = iter_slug.len() - 1;
619      if depth == 0 {
620        // Prefix Chapters
621        // See: https://rust-lang.github.io/mdBook/format/summary.html#structure
622      }
623      let mut subdir_path = slugs_to_paths
624        .get(&iter_slug[0..iter_slug.len() - 1])
625        .cloned()
626        .unwrap_or_else(|| dir_path.clone());
627      let number = iter_slug.last().expect("must get default name");
628      let name = if name.is_empty() {
629        format!("{}", number)
630      } else {
631        format!("{} - {}", number, name)
632      };
633      let mut contents_name = iter_slug[1..]
634        .iter()
635        .fold(iter_slug[0].to_string(), |s, ii| format!("{}.{}", s, ii));
636      match documentable {
637        Documentable::Doc(ref doc_name, ref contents) => {
638          subdir_path.push(name.clone());
639          subdir_path.set_extension("md");
640          let out_contents = format!("# {}  \n{}", name, contents);
641          fs::write(subdir_path.clone(), out_contents)
642            .with_context(|| format!("must write to file {:?}", subdir_path))?;
643          if !doc_name.is_empty() {
644            contents_name.push_str(&format!(" - {}", doc_name));
645          }
646          summary_md_contents.push_str(&format!(
647            "\n{}- [{}](<{}>)  ",
648            indent_for_depth(depth),
649            contents_name,
650            subdir_path
651              .strip_prefix(dir_path.clone())
652              .with_context(|| "Must create subdir path for summary.md".to_string())?
653              .to_string_lossy()
654          ));
655        }
656        Documentable::BoxedDocDict(chapter_name, boxed_doc_dict) => {
657          // update path
658          match &naming_scheme {
659            // Use the deepest-depth chapter name
660            DirectoryNamingScheme::ChapterName => {
661              subdir_path.push(name.clone());
662            }
663            // Use the deepest-depth chapter number
664            DirectoryNamingScheme::ChapterNumber => {
665              subdir_path.push(
666                iter_slug
667                  .iter()
668                  .last()
669                  .expect("must get last slug element")
670                  .to_string(),
671              );
672            }
673          }
674          // create folder
675          fs::create_dir_all(subdir_path.clone())
676            .with_context(|| format!("Must create subdir path {:?}", subdir_path))?;
677          slugs_to_paths.insert(iter_slug.clone(), subdir_path.to_path_buf());
678          // add chapter level-readme entry to summary
679          let mut chapter_readme_path = subdir_path.clone();
680          chapter_readme_path.push(README_NAME);
681          chapter_readme_path.set_extension("md");
682          contents_name.push_str(&format!(" - {}", chapter_name));
683          summary_md_contents.push_str(&format!(
684            "\n{}- [{}](<{}>)  ",
685            indent_for_depth(depth),
686            contents_name,
687            chapter_readme_path
688              .strip_prefix(dir_path.clone())
689              .with_context(|| format!(
690                "Must create relative chapter readme path from {:?}",
691                dir_path
692              ))?
693              .to_string_lossy()
694          ));
695          // create chapter-level readme
696          let mut chapter_readme_contents = format!("# {}", chapter_name);
697
698          if !boxed_doc_dict.1.is_empty() {
699            // Chapter blurbs
700            for (chapter_i, chapter_documentable) in boxed_doc_dict.0.iter() {
701              let mut forward_link: PathBuf = match &naming_scheme {
702                // Use the deepest-depth chapter name
703                DirectoryNamingScheme::ChapterName => {
704                  let chapter_name = chapter_documentable.1.name();
705                  if chapter_name.is_empty() {
706                    format!("{}", chapter_i)
707                  } else {
708                    format!("{} - {}", chapter_i, chapter_name)
709                  }
710                }
711                // Use the deepest-depth chapter number
712                DirectoryNamingScheme::ChapterNumber => chapter_i.to_string(),
713              }
714              .into();
715              forward_link.set_extension("md");
716              let chapter_readme_blurb = boxed_doc_dict
717                .1
718                .iter()
719                .find(|(backlink, _blurb)| backlink == &iter_slug)
720                .map(|(_backlink, blurb)| blurb.to_string())
721                .unwrap_or_else(|| {
722                  format!(
723                    "\n- [Skip to {} ({})](<{}>)  ",
724                    chapter_documentable.0,
725                    chapter_i,
726                    forward_link.to_string_lossy(),
727                  )
728                });
729
730              chapter_readme_contents.push_str(&format!("\n{}", chapter_readme_blurb));
731            }
732          }
733          fs::write(chapter_readme_path.clone(), chapter_readme_contents)
734            .with_context(|| format!("must write to summary.md file {:?}", chapter_readme_path))?;
735        }
736      }
737    }
738
739    // create summary md
740    let mut summary_md_path = dir_path;
741    summary_md_path.push("SUMMARY");
742    summary_md_path.set_extension("md");
743    fs::write(summary_md_path.clone(), summary_md_contents)
744      .with_context(|| format!("must write to summary.md file {:?}", summary_md_path))?;
745    Ok(())
746  }
747
748  /// Find the next available (unused) entry number in a documentation dictionary
749  pub fn find_next_entry_number(&self) -> usize {
750    let mut n = 0usize;
751
752    while self.keys().cloned().any(|x| x == n) {
753      n += 1;
754    }
755    n
756  }
757
758  #[allow(clippy::unnecessary_unwrap)]
759  /// Get an immutable reference to an entry at the specified numeric path. Returns `None` if the
760  /// a path is not present.
761  pub fn get_entry_at_numeric_path(
762    &self,
763    path: &[usize],
764  ) -> Option<&DocDictEntryValueType> {
765    let mut map_pointer: &Self = self;
766    let max_depth = path.len() - 1;
767    for (depth, index) in path.iter().enumerate() {
768      let entry_opt = map_pointer.get(index);
769      if entry_opt.is_some() {
770        if depth == max_depth {
771          // this must be the record
772          return entry_opt;
773        } else {
774          match &entry_opt.unwrap().1 {
775            Documentable::Doc(_, _) => {
776              // no further traversal is possible
777              return None;
778            }
779            Documentable::BoxedDocDict(_name, ref boxed_sub_dict) => {
780              // point to the next depth for further traversal
781              map_pointer = boxed_sub_dict;
782            }
783          }
784        }
785      } else {
786        return None;
787      }
788    }
789    None
790  }
791
792  #[allow(clippy::unnecessary_unwrap)]
793  /// Get a mutable reference to an entry at the specified numeric path. Returns `None` if the
794  /// a path is not present.
795  pub fn get_mut_entry_at_numeric_path(
796    &mut self,
797    path: &[usize],
798  ) -> Option<&mut DocDictEntryValueType> {
799    let mut map_pointer: &mut Self = self;
800    let max_depth = path.len() - 1;
801    for (depth, index) in path.iter().enumerate() {
802      let entry_opt = map_pointer.get_mut(index);
803      if entry_opt.is_some() {
804        if depth == max_depth {
805          // this must be the record
806          return entry_opt;
807        } else {
808          match &mut entry_opt.unwrap().1 {
809            Documentable::Doc(_, _) => {
810              // no further traversal is possible
811              return None;
812            }
813            Documentable::BoxedDocDict(_name, ref mut boxed_sub_dict) => {
814              // point to the next depth for further traversal
815              map_pointer = &mut *boxed_sub_dict;
816            }
817          }
818        }
819      } else {
820        return None;
821      }
822    }
823    None
824  }
825}
826impl core::ops::Deref for DocDict {
827  type Target = DocDictTree;
828  fn deref(&self) -> &Self::Target {
829    &self.0
830  }
831}
832impl core::ops::DerefMut for DocDict {
833  fn deref_mut(&mut self) -> &mut Self::Target {
834    &mut self.0
835  }
836}
837
838/// Get the persistence directory path
839pub fn get_persistence_dir_path() -> PathBuf {
840  std::env::temp_dir().join(OUTPUT_DIR_PATH)
841}
842
843/// Get the full persistence directory+file path
844pub fn get_persistence_file_path() -> anyhow::Result<PathBuf> {
845  CUSTOM_OUTPUT_FILE_NAME.read().map_or_else(
846    |_| anyhow::bail!("Must get read lock on CUSTOM_OUTPUT_FILE_NAME"),
847    |custom_output_file_name_lock| {
848      Ok(
849        get_persistence_dir_path().join(
850          custom_output_file_name_lock
851            .as_ref()
852            .cloned()
853            .unwrap_or_else(|| OUTPUT_FILE_NAME.to_string()),
854        ),
855      )
856    },
857  )
858}
859
860/// Make a string from a DocDict key
861pub fn make_string_key(
862  num: usize,
863  name: String,
864) -> String {
865  format!("({})({})", num, name)
866}
867
868/// Save the global store to a file
869///
870/// - This is necessary because the global store cannot persist between compilation time and runtime
871/// - If no directory is specified it will create a directory that includes the name of the
872/// crate from which the proc macro is called
873/// See [OUTPUT_DIR_PATH]
874pub fn persist_docs() -> anyhow::Result<()> {
875  use anyhow::Context;
876  let dir_path = get_persistence_dir_path();
877  // Create the path if it doesn't exist
878  if !dir_path.is_dir() {
879    fs::create_dir_all(dir_path)?;
880  }
881  let complete_path: PathBuf = get_persistence_file_path()?;
882  // std::println!("saving to {:?}", complete_path);
883  let file = File::create(complete_path)?;
884  let docs = &*DOCS;
885  let docs_read_lock = docs
886    .read()
887    .map_err(|poison_error| anyhow::anyhow!(format!("{:#?}", poison_error)))
888    .with_context(|| "Must get read lock on DOCS")?;
889  // std::println!("{:#?}", docs_read_lock );
890  serde_json::to_writer(file, &*docs_read_lock).with_context(|| {
891    format!(
892      "Must write JSON from DOCS to file: \nResult:\n{:#?}",
893      serde_json::to_string(&*docs_read_lock)
894    )
895  })?;
896  Ok(())
897}
898
899/// Load the docs from a file path
900///
901/// - If given a DocDict, it will load into that DocDict. Otherwise, it will load
902/// into the global doc dict, overwriting  
903/// This is necessary because the global store cannot persist between compilation time
904/// and runtime.
905pub fn load_global_docs(
906  file_name_opt: Option<&str>,
907  doc_dict_opt: Option<&mut DocDict>,
908) -> anyhow::Result<()> {
909  use anyhow::Context;
910  let complete_path: PathBuf = if let Some(file_name) = file_name_opt {
911    get_persistence_dir_path().join(file_name)
912  } else {
913    get_persistence_file_path()?
914  };
915  // Fail if the path doesn't exist
916  if !complete_path.is_file() {
917    anyhow::bail!(format!(
918      "The target docs directory ({:?}) is not a file",
919      complete_path
920    ));
921  }
922  std::println!("loading global docs from complete_path {:?}", complete_path);
923  let file = File::open(complete_path)?;
924  let file_reader = BufReader::new(file);
925  let docs = &*DOCS;
926  let doc_dict: DocDict = serde_json::from_reader(file_reader)
927    .with_context(|| "Must read JSON from file into docs".to_string())?;
928  match doc_dict_opt {
929    Some(existing_doc_dict) => {
930      *existing_doc_dict = doc_dict;
931    }
932    None => {
933      let mut docs_write_lock = docs
934        .write()
935        .map_err(|poison_error| anyhow::anyhow!(format!("{:#?}", poison_error)))
936        .with_context(|| "Must get write lock on DOCS")?;
937      // std::println!("docs_write_lock {:#?}", docs_write_lock );
938      *docs_write_lock = doc_dict;
939    }
940  }
941  Ok(())
942}
943
944#[cfg(test)]
945mod tests {
946  use super::*;
947
948  #[test]
949  fn deep_iter() {
950    let mut d: DocDict = DocDict(
951      BTreeMap::new(),
952      vec![
953        (
954          vec![],
955          String::from("A book about some things. Words sdrwo."),
956        ),
957        (vec![], String::from("\nAnd some other things.")),
958      ],
959    );
960
961    let slugs = vec![
962      vec![
963        (1, "buff".to_string()),
964        (2, "aztec".to_string()),
965        (3, "priestess".to_string()),
966      ],
967      vec![
968        (1, "buff".to_string()),
969        (2, "aztec".to_string()),
970        (4, "priest".to_string()),
971      ],
972      vec![
973        (1, "buff".to_string()),
974        (2, "aztec".to_string()),
975        (5, "eagle warrior".to_string()),
976      ],
977      vec![
978        (1, "buff".to_string()),
979        (3, "maya".to_string()),
980        (2, "princess".to_string()),
981      ],
982      vec![
983        (1, "buff".to_string()),
984        (3, "maya".to_string()),
985        (5, "prince".to_string()),
986      ],
987    ];
988    let target_ord = vec![
989      slugs[0][0].clone(),
990      slugs[0][1].clone(),
991      slugs[0][2].clone(),
992      slugs[1][2].clone(),
993      slugs[2][2].clone(),
994      slugs[3][1].clone(),
995      slugs[3][2].clone(),
996      slugs[4][2].clone(),
997    ];
998    let target_num_slugs = vec![
999      vec![1],
1000      vec![1, 2],
1001      vec![1, 2, 3],
1002      vec![1, 2, 4],
1003      vec![1, 2, 5],
1004      vec![1, 3],
1005      vec![1, 3, 2],
1006      vec![1, 3, 5],
1007    ];
1008    let target_sub_entries_lens = vec![2, 3, 0, 0, 0, 2, 0, 0];
1009    for slug in slugs {
1010      let (path_numbers, path_names): (Vec<_>, Vec<_>) = slug.iter().cloned().unzip();
1011      let name = slug.last().unwrap().1.to_string();
1012      d.add_path(
1013        &None,
1014        &Some(name.clone()),
1015        Some(Documentable::Doc(name.clone(), "dummy".to_string())),
1016        None,
1017        &path_names,
1018        &path_numbers,
1019      )
1020      .expect("must add path");
1021    }
1022    for (i, (iter_slug, (name, documentable), sub_entries_len)) in d.deep_iter(None).enumerate() {
1023      let target = &target_ord[i];
1024      let target_num_slug = &target_num_slugs[i];
1025      let target_sub_entries_len = target_sub_entries_lens[i];
1026      assert_eq!(&target.1, name, "{} th target name must match", i);
1027      assert_eq!(
1028        target_num_slug, &iter_slug,
1029        "{} th target slug must match",
1030        i
1031      );
1032      assert_eq!(
1033        target_sub_entries_len, sub_entries_len,
1034        "{} th target sub entries length must match",
1035        i
1036      );
1037
1038      std::println!("{} {}", i, documentable);
1039    }
1040  }
1041}