1use crate::{
2 custom_types,
3 export::{ItemsOrder, Options, RHAI_ITEM_INDEX_PATTERN},
4 function,
5 module::Error,
6};
7use serde::ser::SerializeStruct;
8
9#[derive(Debug, Clone)]
11pub enum Item {
12 Function {
13 root_metadata: function::Metadata,
14 metadata: Vec<function::Metadata>,
15 name: String,
16 index: usize,
17 },
18 CustomType {
19 metadata: custom_types::Metadata,
20 index: usize,
21 },
22}
23
24impl serde::Serialize for Item {
25 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
26 where
27 S: serde::Serializer,
28 {
29 match self {
30 Self::Function {
31 root_metadata,
32 name,
33 metadata,
34 ..
35 } => {
36 let mut state = serializer.serialize_struct("item", 4)?;
37 state.serialize_field(
38 "type",
39 root_metadata.generate_function_definition().type_to_str(),
40 )?;
41 state.serialize_field("heading_id", &self.heading_id())?;
42 state.serialize_field("name", name)?;
43 state.serialize_field(
44 "signatures",
45 metadata
46 .iter()
47 .map(|metadata| metadata.generate_function_definition().display())
48 .collect::<Vec<_>>()
49 .join("\n")
50 .as_str(),
51 )?;
52 state.serialize_field("sections", {
53 &Section::extract_sections(
54 &root_metadata
55 .doc_comments
56 .clone()
57 .unwrap_or_default()
58 .join("\n"),
59 )
60 })?;
61 state.end()
62 }
63 Self::CustomType { metadata, .. } => {
64 let mut state = serializer.serialize_struct("item", 2)?;
65 state.serialize_field("name", &metadata.display_name)?;
66 state.serialize_field("heading_id", &self.heading_id())?;
67 state.serialize_field(
68 "sections",
69 &Section::extract_sections(
70 &metadata.doc_comments.clone().unwrap_or_default().join("\n"),
71 ),
72 )?;
73 state.end()
74 }
75 }
76 }
77}
78
79impl Item {
80 pub(crate) fn new_function(
81 metadata: &[function::Metadata],
82 name: &str,
83 options: &Options,
84 ) -> Result<Option<Self>, Error> {
85 let root = metadata
87 .iter()
88 .find(|metadata| metadata.doc_comments.is_some());
89
90 match root {
91 Some(root) if !name.starts_with("anon$") => {
93 if matches!(options.items_order, ItemsOrder::ByIndex) {
94 Self::find_index(root.doc_comments.as_ref().unwrap_or(&vec![]))?
95 } else {
96 Some(0)
97 }
98 .map_or_else(
99 || Ok(None),
100 |index| {
101 Ok(Some(Self::Function {
102 root_metadata: root.clone(),
103 metadata: metadata.to_vec(),
104 name: name.to_string(),
105 index,
106 }))
107 },
108 )
109 }
110 _ => Ok(None),
111 }
112 }
113
114 pub(crate) fn new_custom_type(
115 metadata: custom_types::Metadata,
116 options: &Options,
117 ) -> Result<Option<Self>, Error> {
118 if matches!(options.items_order, ItemsOrder::ByIndex) {
119 Self::find_index(metadata.doc_comments.as_ref().unwrap_or(&vec![]))?
120 } else {
121 Some(0)
122 }
123 .map_or_else(
124 || Ok(None),
125 |index| Ok(Some(Self::CustomType { metadata, index })),
126 )
127 }
128
129 #[must_use]
131 pub const fn index(&self) -> usize {
132 match self {
133 Self::CustomType { index, .. } | Self::Function { index, .. } => *index,
134 }
135 }
136
137 #[must_use]
139 pub fn name(&self) -> &str {
140 match self {
141 Self::CustomType { metadata, .. } => metadata.display_name.as_str(),
142 Self::Function { name, .. } => name,
143 }
144 }
145
146 #[must_use]
148 pub fn heading_id(&self) -> String {
149 let prefix = match self {
150 Self::Function { root_metadata, .. } => root_metadata
151 .generate_function_definition()
152 .type_to_str()
153 .replace(['/', ' '], ""),
154 Self::CustomType { .. } => "type".to_string(),
155 };
156
157 format!("{prefix}-{}", self.name())
158 }
159
160 pub(crate) fn find_index(doc_comments: &[String]) -> Result<Option<usize>, Error> {
162 for line in doc_comments {
163 if let Some((_, index)) = line.rsplit_once(RHAI_ITEM_INDEX_PATTERN) {
164 return index
165 .parse::<usize>()
166 .map_err(Error::ParseOrderMetadata)
167 .map(Some);
168 }
169 }
170
171 Ok(None)
172 }
173
174 pub(crate) fn format_comments(doc_comments: &[String]) -> String {
177 let doc_comments = doc_comments.to_vec();
178 let removed_extra_tokens = Self::remove_extra_tokens(doc_comments).join("\n");
179 let remove_comments = Self::fmt_doc_comments(&removed_extra_tokens);
180
181 Self::remove_test_code(&remove_comments)
182 }
183
184 pub(crate) fn remove_extra_tokens(dc: Vec<String>) -> Vec<String> {
186 dc.into_iter()
187 .map(|s| {
188 s.lines()
189 .filter(|l| !l.contains(RHAI_ITEM_INDEX_PATTERN))
190 .collect::<Vec<_>>()
191 .join("\n")
192 })
193 .collect::<Vec<_>>()
194 }
195
196 pub(crate) fn fmt_doc_comments(dc: &str) -> String {
198 dc.replace("/// ", "")
199 .replace("///", "")
200 .replace("/**", "")
201 .replace("**/", "")
202 .replace("**/", "")
203 }
204
205 pub(crate) fn remove_test_code(doc_comments: &str) -> String {
210 let mut formatted = vec![];
211 let mut in_code_block = false;
212 for line in doc_comments.lines() {
213 if line.starts_with("```") {
214 in_code_block = !in_code_block;
215 formatted.push(line);
216 continue;
217 }
218
219 if !(in_code_block && line.starts_with("# ")) {
220 formatted.push(line);
221 }
222 }
223
224 formatted.join("\n")
225 }
226}
227
228#[derive(Default, Clone, serde::Serialize)]
229struct Section {
230 pub name: String,
231 pub body: String,
232}
233
234impl Section {
235 fn extract_sections(docs: &str) -> Vec<Self> {
236 let mut sections = vec![];
237 let mut current_name = "Description".to_string();
238 let mut current_body = vec![];
239 let mut in_code_block = false;
240
241 docs.lines().for_each(|line| {
243 if line.split_once("```").is_some() {
244 in_code_block = !in_code_block;
245 }
246
247 match line.split_once("# ") {
248 Some((_prefix, name))
249 if !in_code_block && !line.contains(RHAI_ITEM_INDEX_PATTERN) =>
250 {
251 sections.push(Self {
252 name: std::mem::take(&mut current_name),
253 body: Item::format_comments(¤t_body[..]),
254 });
255
256 current_name = name.to_string();
257 current_body = vec![];
258 }
259 Some(_) | None => current_body.push(format!("{line}\n")),
260 }
261 });
262
263 if !current_body.is_empty() {
264 sections.push(Self {
265 name: std::mem::take(&mut current_name),
266 body: Item::format_comments(¤t_body[..]),
267 });
268 }
269
270 sections
271 }
272}
273
274#[cfg(test)]
275pub mod test {
276 use super::*;
277
278 #[test]
279 fn test_remove_test_code_simple() {
280 pretty_assertions::assert_eq!(
281 Item::remove_test_code(
282 r"
283# Not removed.
284```
285fn my_func(a: int) -> () {}
286do stuff ...
287# Please hide this.
288do something else ...
289# Also this.
290```
291# Not removed either.
292",
293 ),
294 r"
295# Not removed.
296```
297fn my_func(a: int) -> () {}
298do stuff ...
299do something else ...
300```
301# Not removed either.",
302 );
303 }
304
305 #[test]
306 fn test_remove_test_code_multiple_blocks() {
307 pretty_assertions::assert_eq!(
308 Item::remove_test_code(
309 r"
310```ignore
311block 1
312# Please hide this.
313```
314
315# A title
316
317```
318block 2
319# Please hide this.
320john
321doe
322# To hide.
323```
324",
325 ),
326 r"
327```ignore
328block 1
329```
330
331# A title
332
333```
334block 2
335john
336doe
337```",
338 );
339 }
340
341 #[test]
342 fn test_remove_test_code_with_rhai_map() {
343 pretty_assertions::assert_eq!(
344 Item::remove_test_code(
345 r#"
346```rhai
347#{
348 "a": 1,
349 "b": 2,
350 "c": 3,
351};
352# Please hide this.
353```
354
355# A title
356
357```
358# Please hide this.
359let map = #{
360 "hello": "world"
361# To hide.
362};
363# To hide.
364```
365"#,
366 ),
367 r#"
368```rhai
369#{
370 "a": 1,
371 "b": 2,
372 "c": 3,
373};
374```
375
376# A title
377
378```
379let map = #{
380 "hello": "world"
381};
382```"#,
383 );
384 }
385}