starlark/docs/
multipage.rs

1/*
2 * Copyright 2019 The Starlark in Rust Authors.
3 * Copyright (c) Facebook, Inc. and its affiliates.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18use std::collections::HashMap;
19
20use dupe::Dupe;
21
22use crate::docs::DocItem;
23use crate::docs::DocModule;
24use crate::docs::DocType;
25use crate::typing::ty::TypeRenderConfig;
26use crate::typing::Ty;
27use crate::typing::TyBasic;
28use crate::typing::TyStarlarkValue;
29
30pub struct DocModuleInfo<'a> {
31    pub module: &'a DocModule,
32    pub name: String,
33    /// A prefix to attach to all of the pages rendered from this module
34    pub page_path: String,
35}
36
37impl<'a> DocModuleInfo<'a> {
38    fn into_page_renders(&self) -> Vec<PageRender<'a>> {
39        Self::traverse_inner(&self.module, &self.name, &self.page_path)
40    }
41
42    fn traverse_inner(
43        docs: &'a DocModule,
44        module_name: &str,
45        base_path: &str,
46    ) -> Vec<PageRender<'a>> {
47        let mut result = vec![];
48
49        result.push(PageRender {
50            page: DocPageRef::Module(docs),
51            path: base_path.to_owned(),
52            name: module_name.to_owned(),
53            ty: None,
54        });
55
56        for (name, doc) in &docs.members {
57            let path = if base_path.is_empty() {
58                name.to_owned()
59            } else {
60                format!("{}/{}", base_path, name)
61            };
62            match doc {
63                DocItem::Module(doc_module) => {
64                    result.extend(Self::traverse_inner(&doc_module, &name, &path))
65                }
66                DocItem::Type(doc_type) => result.push(PageRender {
67                    page: DocPageRef::Type(doc_type),
68                    path,
69                    name: name.to_owned(),
70                    ty: Some(doc_type.ty.dupe()),
71                }),
72
73                DocItem::Member(_) => (),
74            }
75        }
76
77        result
78    }
79}
80
81/// A reference to a page to render
82/// DocsRender will have all the PageRender it needs to render the docs
83/// Since types and some modules are owned by other modules, we need to use the reference here
84enum DocPageRef<'a> {
85    Module(&'a DocModule),
86    Type(&'a DocType),
87}
88
89/// A single page to render
90struct PageRender<'a> {
91    page: DocPageRef<'a>,
92    path: String,
93    name: String,
94    /// The type of the page, if it is a type page. This is used to get the link to the type.
95    ty: Option<Ty>,
96}
97
98impl<'a> PageRender<'a> {
99    fn render_markdown(&self, render_config: &TypeRenderConfig) -> String {
100        match self.page {
101            DocPageRef::Module(doc_module) => {
102                doc_module.render_markdown_page_for_multipage_render(&self.name, render_config)
103            }
104            DocPageRef::Type(doc_type) => {
105                doc_type.render_markdown_page_for_multipage_render(&self.name, render_config)
106            }
107        }
108    }
109}
110
111/// Renders the contents into a multi-page tree structure
112///
113/// The output will contain page-paths like ``, `type1`, `mod1`, and `mod1/type2`, each mapped to
114/// the contents of that page. That means that some of the paths may be prefixes of each other,
115/// which will need consideration if this is to be materialized to a filesystem.
116struct MultipageRender<'a> {
117    page_renders: Vec<PageRender<'a>>,
118    // used for the linkable type in the markdown
119    render_config: TypeRenderConfig,
120}
121
122impl<'a> MultipageRender<'a> {
123    /// Create a new MultipageRender from a list of DocModuleInfo, and an optional function to map a type path to a linkable path
124    /// If the function is not provided, the type will not be linkable
125    /// linked_ty_mapper is used to map the **type path** and **type name** to a linkable element in the markdown
126    fn new(
127        docs: Vec<DocModuleInfo<'a>>,
128        linked_ty_mapper: Option<fn(&str, &str) -> String>,
129    ) -> Self {
130        let mut res = vec![];
131        for doc in docs {
132            res.extend(doc.into_page_renders());
133        }
134        let mut render_config = TypeRenderConfig::Default;
135        if let Some(linked_ty_mapper) = linked_ty_mapper {
136            let mut ty_to_path_map = HashMap::new();
137            for page in res.iter() {
138                if let Some(ty) = &page.ty {
139                    ty_to_path_map.insert(ty.dupe(), page.path.clone());
140                }
141            }
142
143            let render_linked_ty_starlark_value = move |ty: &TyStarlarkValue| {
144                let type_name = ty.to_string();
145                if let Some(type_path) =
146                    ty_to_path_map.get(&Ty::basic(TyBasic::StarlarkValue(ty.dupe())))
147                {
148                    linked_ty_mapper(type_path, &type_name)
149                } else {
150                    type_name.to_owned()
151                }
152            };
153
154            render_config = TypeRenderConfig::LinkedType {
155                render_linked_ty_starlark_value: Box::new(render_linked_ty_starlark_value),
156            };
157        }
158        Self {
159            page_renders: res,
160            render_config,
161        }
162    }
163
164    /// Render the docs into a map of markdown paths to markdown content
165    fn render_markdown_pages(&self) -> HashMap<String, String> {
166        self.page_renders
167            .iter()
168            .map(|page| (page.path.clone(), page.render_markdown(&self.render_config)))
169            .collect()
170    }
171}
172
173/// Renders the contents into a multi-page tree structure
174///
175/// The output will contain page-paths like ``, `type1`, `mod1`, and `mod1/type2`, each mapped to
176/// the contents of that page. That means that some of the paths may be prefixes of each other,
177/// which will need consideration if this is to be materialized to a filesystem.
178///
179/// It accepts a list of DocModuleInfo, and an optional function linked_ty_mapper
180/// linked_ty_mapper is used to map the **type path** and **type name** to a linkable element in the markdown
181pub fn render_markdown_multipage(
182    modules_infos: Vec<DocModuleInfo<'_>>,
183    linked_ty_mapper: Option<fn(&str, &str) -> String>,
184) -> HashMap<String, String> {
185    let multipage_render = MultipageRender::new(modules_infos, linked_ty_mapper);
186    multipage_render.render_markdown_pages()
187}