tinymist_analysis/docs/
tidy.rs

1//! The documentation models for tidy.
2
3use ecow::EcoString;
4use itertools::Itertools;
5use serde::{Deserialize, Serialize};
6use typst::diag::StrResult;
7
8/// A parameter documentation.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TidyParamDocs {
11    /// The name of the parameter.
12    pub name: EcoString,
13    /// The documentation of the parameter.
14    pub docs: EcoString,
15    /// The types of the parameter.
16    pub types: EcoString,
17    /// The default value of the parameter.
18    pub default: Option<EcoString>,
19}
20
21/// A pattern documentation.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct TidyPatDocs {
24    /// The documentation of the pattern.
25    pub docs: EcoString,
26    /// The return type of the pattern.
27    pub return_ty: Option<EcoString>,
28    /// The parameters of the pattern.
29    pub params: Vec<TidyParamDocs>,
30}
31
32/// A module documentation.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TidyModuleDocs {
35    /// The documentation of the module.
36    pub docs: EcoString,
37}
38
39/// Removes the list annotations from the string.
40pub fn remove_list_annotations(s: &str) -> String {
41    static REG: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
42        regex::Regex::new(r"<!-- typlite:(?:begin|end):[\w\-]+ \d+ -->").unwrap()
43    });
44    REG.replace_all(s, "").to_string()
45}
46
47/// Identifies the pattern documentation. For example, `#let (a, b) = x`.
48pub fn identify_pat_docs(converted: &str) -> StrResult<TidyPatDocs> {
49    let lines = converted.lines().collect::<Vec<_>>();
50
51    let mut matching_return_ty = true;
52    let mut buf = vec![];
53    let mut params = vec![];
54    let mut return_ty = None;
55    let mut break_line = None;
56
57    let mut line_width = lines.len();
58    'search: loop {
59        if line_width == 0 {
60            break;
61        }
62        line_width -= 1;
63
64        let line = lines[line_width];
65        if line.is_empty() {
66            continue;
67        }
68
69        loop {
70            if matching_return_ty {
71                matching_return_ty = false;
72                let line = line.trim_start();
73                let type_line = line
74                    .strip_prefix("-\\>")
75                    .or_else(|| line.strip_prefix("->"));
76                let Some(w) = type_line else {
77                    // break_line = Some(i);
78                    continue;
79                };
80
81                break_line = Some(line_width);
82                return_ty = Some(w.trim().into());
83                break;
84            }
85
86            let Some(mut line) = line
87                .trim_end()
88                .strip_suffix("<!-- typlite:end:list-item 0 -->")
89            else {
90                break_line = Some(line_width + 1);
91                break 'search;
92            };
93            let mut current_line_no = line_width;
94
95            loop {
96                // <!-- typlite:begin:list-item -->
97                let t = line
98                    .trim_start()
99                    .strip_prefix("- ")
100                    .and_then(|t| t.trim().strip_prefix("<!-- typlite:begin:list-item 0 -->"));
101
102                let line_content = match t {
103                    Some(t) => {
104                        buf.push(t);
105                        break;
106                    }
107                    None => line,
108                };
109
110                buf.push(line_content);
111
112                if current_line_no == 0 {
113                    break_line = Some(line_width + 1);
114                    break 'search;
115                }
116                current_line_no -= 1;
117                line = lines[current_line_no];
118            }
119
120            let mut buf = std::mem::take(&mut buf);
121            buf.reverse();
122
123            let Some(first_line) = buf.first_mut() else {
124                break_line = Some(line_width + 1);
125                break 'search;
126            };
127            *first_line = first_line.trim();
128
129            let Some(param_line) = None.or_else(|| {
130                let (param_name, rest) = first_line.split_once(" ")?;
131                let (type_content, rest) = match_brace(rest.trim_start().strip_prefix("(")?)?;
132                let (_, rest) = rest.split_once(":")?;
133                *first_line = rest.trim();
134                Some((param_name.into(), type_content.into()))
135            }) else {
136                break_line = Some(line_width + 1);
137                break 'search;
138            };
139
140            line_width = current_line_no;
141            params.push(TidyParamDocs {
142                name: param_line.0,
143                types: param_line.1,
144                default: None,
145                docs: remove_list_annotations(&buf.into_iter().join("\n")).into(),
146            });
147            break_line = Some(line_width);
148
149            break;
150        }
151    }
152
153    let docs = match break_line {
154        Some(line_no) => {
155            remove_list_annotations(&(lines[..line_no]).iter().copied().join("\n")).into()
156        }
157        None => remove_list_annotations(converted).into(),
158    };
159
160    params.reverse();
161    Ok(TidyPatDocs {
162        docs,
163        return_ty,
164        params,
165    })
166}
167
168/// Identifies the module documentation.
169pub fn identify_tidy_module_docs(docs: EcoString) -> StrResult<TidyModuleDocs> {
170    Ok(TidyModuleDocs {
171        docs: remove_list_annotations(&docs).into(),
172    })
173}
174
175fn match_brace(trim_start: &str) -> Option<(&str, &str)> {
176    let mut brace_count = 1;
177    let mut end = 0;
178    for (idx, ch) in trim_start.char_indices() {
179        match ch {
180            '(' => brace_count += 1,
181            ')' => brace_count -= 1,
182            _ => {}
183        }
184
185        if brace_count == 0 {
186            end = idx;
187            break;
188        }
189    }
190
191    if brace_count != 0 {
192        return None;
193    }
194
195    let (type_content, rest) = trim_start.split_at(end);
196    Some((type_content, rest))
197}
198
199#[cfg(test)]
200mod tests {
201    use std::fmt::Write;
202
203    use super::TidyParamDocs;
204
205    fn func(s: &str) -> String {
206        let docs = super::identify_pat_docs(s).unwrap();
207        let mut res = format!(">> docs:\n{}\n<< docs", docs.docs);
208        if let Some(t) = docs.return_ty {
209            res.push_str(&format!("\n>>return\n{t}\n<<return"));
210        }
211        for TidyParamDocs {
212            name,
213            types,
214            docs,
215            default: _,
216        } in docs.params
217        {
218            let _ = write!(res, "\n>>arg {name}: {types}\n{docs}\n<< arg");
219        }
220        res
221    }
222
223    fn var(s: &str) -> String {
224        let docs = super::identify_pat_docs(s).unwrap();
225        let mut res = format!(">> docs:\n{}\n<< docs", docs.docs);
226        if let Some(t) = docs.return_ty {
227            res.push_str(&format!("\n>>return\n{t}\n<<return"));
228        }
229        res
230    }
231
232    #[test]
233    fn test_identify_tidy_docs() {
234        insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
235- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
236- <!-- typlite:begin:list-item 0 -->`types` (optional): A list of accepted argument types.<!-- typlite:end:list-item 0 --> 
237- <!-- typlite:begin:list-item 0 -->`default` (optional): Default value for this argument.<!-- typlite:end:list-item 0 -->
238
239See show-module() for outputting the results of this function.
240
241- <!-- typlite:begin:list-item 0 -->content (string): Content of `.typ` file to analyze for docstrings.<!-- typlite:end:list-item 0 -->
242- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 --> 
243- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function 
244        references. If `auto`, the label-prefix name will be the module name.<!-- typlite:end:list-item 0 --> 
245- <!-- typlite:begin:list-item 0 -->require-all-parameters (boolean): Require that all parameters of a 
246        functions are documented and fail if some are not.<!-- typlite:end:list-item 0 --> 
247- <!-- typlite:begin:list-item 0 -->scope (dictionary): A dictionary of definitions that are then available 
248        in all function and parameter descriptions.<!-- typlite:end:list-item 0 --> 
249- <!-- typlite:begin:list-item 0 -->preamble (string): Code to prepend to all code snippets shown with `#example()`. 
250        This can for instance be used to import something from the scope.<!-- typlite:end:list-item 0 --> 
251-> string"###), @r"
252        >> docs:
253        These again are dictionaries with the keys
254        - `description` (optional): The description for the argument.
255        - `types` (optional): A list of accepted argument types. 
256        - `default` (optional): Default value for this argument.
257
258        See show-module() for outputting the results of this function.
259        << docs
260        >>return
261        string
262        <<return
263        >>arg content: string
264        Content of `.typ` file to analyze for docstrings.
265        << arg
266        >>arg name: string
267        The name for the module.
268        << arg
269        >>arg label-prefix: auto, string
270        The label-prefix for internal function
271                references. If `auto`, the label-prefix name will be the module name.
272        << arg
273        >>arg require-all-parameters: boolean
274        Require that all parameters of a
275                functions are documented and fail if some are not.
276        << arg
277        >>arg scope: dictionary
278        A dictionary of definitions that are then available
279                in all function and parameter descriptions.
280        << arg
281        >>arg preamble: string
282        Code to prepend to all code snippets shown with `#example()`.
283                This can for instance be used to import something from the scope.
284        << arg
285        ");
286    }
287
288    #[test]
289    fn test_identify_tidy_docs_nested() {
290        insta::assert_snapshot!(func(r###"These again are dictionaries with the keys
291- <!-- typlite:begin:list-item 0 -->`description` (optional): The description for the argument.<!-- typlite:end:list-item 0 -->
292
293See show-module() for outputting the results of this function.
294
295- <!-- typlite:begin:list-item 0 -->name (string): The name for the module.<!-- typlite:end:list-item 0 --> 
296- <!-- typlite:begin:list-item 0 -->label-prefix (auto, string): The label-prefix for internal function 
297        references. If `auto`, the label-prefix name will be the module name. 
298  - <!-- typlite:begin:list-item 1 -->nested something<!-- typlite:end:list-item 1 -->
299  - <!-- typlite:begin:list-item 1 -->nested something 2<!-- typlite:end:list-item 1 --><!-- typlite:end:list-item 0 -->
300-> string"###), @r"
301        >> docs:
302        These again are dictionaries with the keys
303        - `description` (optional): The description for the argument.
304
305        See show-module() for outputting the results of this function.
306        << docs
307        >>return
308        string
309        <<return
310        >>arg name: string
311        The name for the module.
312        << arg
313        >>arg label-prefix: auto, string
314        The label-prefix for internal function
315                references. If `auto`, the label-prefix name will be the module name. 
316          - nested something
317          - nested something 2
318        << arg
319        ");
320    }
321
322    #[test]
323    fn test_identify_tidy_docs3() {
324        insta::assert_snapshot!(var(r###"See show-module() for outputting the results of this function.
325-> string"###), @r"
326        >> docs:
327        See show-module() for outputting the results of this function.
328        << docs
329        >>return
330        string
331        <<return
332        ");
333    }
334
335    #[test]
336    fn test_identify_tidy_docs4() {
337        insta::assert_snapshot!(func(r###"
338- <!-- typlite:begin:list-item 0 -->fn (function, fn): The `fn`.<!-- typlite:end:list-item 0 -->"###), @r"
339        >> docs:
340
341        << docs
342        >>arg fn: function, fn
343        The `fn`.
344        << arg
345        ");
346    }
347}