tinymist_analysis/docs/
tidy.rs1use ecow::EcoString;
4use itertools::Itertools;
5use serde::{Deserialize, Serialize};
6use typst::diag::StrResult;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TidyParamDocs {
11 pub name: EcoString,
13 pub docs: EcoString,
15 pub types: EcoString,
17 pub default: Option<EcoString>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct TidyPatDocs {
24 pub docs: EcoString,
26 pub return_ty: Option<EcoString>,
28 pub params: Vec<TidyParamDocs>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct TidyModuleDocs {
35 pub docs: EcoString,
37}
38
39pub 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
47pub 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 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 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
168pub 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}