diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/cli.rs | 18 | ||||
-rw-r--r-- | src/doxygen.rs | 454 | ||||
-rw-r--r-- | src/doxygen/builders.rs | 109 | ||||
-rw-r--r-- | src/entities.rs | 134 | ||||
-rw-r--r-- | src/generator.rs | 209 | ||||
-rw-r--r-- | src/main.rs | 44 | ||||
-rw-r--r-- | src/pandoc.rs | 246 | ||||
-rw-r--r-- | src/pandoc/types.rs | 39 | ||||
-rw-r--r-- | src/parsing.rs | 457 |
9 files changed, 1710 insertions, 0 deletions
diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e106de7 --- /dev/null +++ b/src/cli.rs | |||
@@ -0,0 +1,18 @@ | |||
1 | use structopt::StructOpt; | ||
2 | |||
3 | #[derive(Debug, Clone, StructOpt)] | ||
4 | pub(crate) struct Cli { | ||
5 | #[structopt(long, short, parse(from_occurrences))] | ||
6 | pub(crate) verbosity: u8, | ||
7 | |||
8 | #[structopt(long, number_of_values = 1, parse(try_from_str = shell_words::split))] | ||
9 | pub(crate) extra_arg: Vec<Vec<String>>, | ||
10 | |||
11 | #[structopt(subcommand)] | ||
12 | pub(crate) command: Command, | ||
13 | } | ||
14 | |||
15 | #[derive(Debug, Clone, StructOpt)] | ||
16 | pub(crate) enum Command { | ||
17 | Generate { file: String }, | ||
18 | } | ||
diff --git a/src/doxygen.rs b/src/doxygen.rs new file mode 100644 index 0000000..8d2267d --- /dev/null +++ b/src/doxygen.rs | |||
@@ -0,0 +1,454 @@ | |||
1 | /// A 'recursive descent' parser for Doxygen XML files | ||
2 | mod builders; | ||
3 | |||
4 | use xml::{ | ||
5 | attribute::OwnedAttribute, | ||
6 | reader::{EventReader, ParserConfig, XmlEvent}, | ||
7 | }; | ||
8 | |||
9 | use std::collections::HashMap; | ||
10 | use std::io::Read; | ||
11 | |||
12 | // === Types === | ||
13 | |||
14 | #[derive(Debug, Clone)] | ||
15 | pub(crate) enum DoxygenType { | ||
16 | NameSpace(NameSpace), | ||
17 | Class(Class), | ||
18 | Unhandled, | ||
19 | } | ||
20 | |||
21 | #[derive(Debug, Clone)] | ||
22 | pub(crate) struct NameSpace { | ||
23 | pub(crate) name: String, | ||
24 | pub(crate) members: Vec<String>, | ||
25 | pub(crate) brief_description: Description, | ||
26 | pub(crate) detailed_description: Description, | ||
27 | } | ||
28 | |||
29 | #[derive(Debug, Clone)] | ||
30 | pub(crate) struct FunctionArgument { | ||
31 | pub(crate) name: String, | ||
32 | pub(crate) r#type: String, | ||
33 | pub(crate) default_value: Option<String>, | ||
34 | } | ||
35 | |||
36 | #[derive(Debug, Clone)] | ||
37 | pub(crate) enum Member { | ||
38 | Variable { | ||
39 | name: String, | ||
40 | r#type: String, | ||
41 | }, | ||
42 | // What | ||
43 | /* | ||
44 | Enum { | ||
45 | |||
46 | } | ||
47 | */ | ||
48 | Function { | ||
49 | name: String, | ||
50 | args: Vec<FunctionArgument>, | ||
51 | return_type: String, | ||
52 | }, | ||
53 | Unhandled, | ||
54 | } | ||
55 | |||
56 | #[derive(Debug, Clone)] | ||
57 | pub(crate) struct Class { | ||
58 | pub(crate) name: String, | ||
59 | pub(crate) inners: Vec<(String, String)>, | ||
60 | pub(crate) sections: HashMap<String, Vec<Member>>, | ||
61 | pub(crate) brief_description: Description, | ||
62 | pub(crate) detailed_description: Description, | ||
63 | } | ||
64 | |||
65 | // === Description === | ||
66 | |||
67 | #[derive(Debug, Clone)] | ||
68 | pub(crate) struct Description { | ||
69 | pub(crate) inner: Vec<DescriptionNode>, | ||
70 | } | ||
71 | |||
72 | impl Description { | ||
73 | fn new() -> Self { | ||
74 | Self { inner: Vec::new() } | ||
75 | } | ||
76 | } | ||
77 | |||
78 | #[derive(Debug, Clone)] | ||
79 | pub(crate) enum DescriptionNode { | ||
80 | Text(String), | ||
81 | Title(String), | ||
82 | Para(Paragraph), | ||
83 | Sect1(String), | ||
84 | Internal(String), | ||
85 | } | ||
86 | |||
87 | #[derive(Debug, Clone)] | ||
88 | pub(crate) struct Paragraph { | ||
89 | pub(crate) inner: Vec<ParagraphNode>, | ||
90 | } | ||
91 | |||
92 | #[derive(Debug, Clone)] | ||
93 | pub(crate) enum ParagraphNode { | ||
94 | Text(String), | ||
95 | Ref(Ref), | ||
96 | } | ||
97 | |||
98 | #[derive(Debug, Clone)] | ||
99 | pub(crate) struct Ref { | ||
100 | pub(crate) id: String, | ||
101 | pub(crate) kind: RefKind, | ||
102 | pub(crate) text: String, | ||
103 | } | ||
104 | |||
105 | #[derive(Debug, Clone)] | ||
106 | pub(crate) enum RefKind { | ||
107 | Compound, | ||
108 | Member, | ||
109 | } | ||
110 | |||
111 | #[derive(Debug, Clone)] | ||
112 | enum DoxygenTypeKind { | ||
113 | NameSpace, | ||
114 | Unhandled, | ||
115 | } | ||
116 | |||
117 | trait TypeBuilder { | ||
118 | //const KIND: DoxygenTypeKind; | ||
119 | |||
120 | fn build(self: Box<Self>) -> DoxygenType; | ||
121 | |||
122 | fn name(&mut self, value: String); | ||
123 | fn brief_description(&mut self, description: Description); | ||
124 | fn detailed_description(&mut self, description: Description); | ||
125 | |||
126 | fn add_inner(&mut self, name: String, kind: String); | ||
127 | fn add_member(&mut self, section: String, member: Member); | ||
128 | } | ||
129 | |||
130 | trait FromXML { | ||
131 | fn from_xml(attributes: Vec<OwnedAttribute>, event_reader: &mut EventReader<impl Read>) | ||
132 | -> Self; | ||
133 | } | ||
134 | |||
135 | impl FromXML for Description { | ||
136 | fn from_xml( | ||
137 | _attributes: Vec<OwnedAttribute>, | ||
138 | mut event_reader: &mut EventReader<impl Read>, | ||
139 | ) -> Self { | ||
140 | let mut inner = Vec::new(); | ||
141 | |||
142 | while let Ok(event) = event_reader.next() { | ||
143 | match event { | ||
144 | XmlEvent::Characters(text) => inner.push(DescriptionNode::Text(text)), | ||
145 | XmlEvent::StartElement { | ||
146 | name, attributes, .. | ||
147 | } => inner.push(match name.local_name.as_str() { | ||
148 | "para" => { | ||
149 | DescriptionNode::Para(Paragraph::from_xml(attributes, &mut event_reader)) | ||
150 | } | ||
151 | "title" => DescriptionNode::Title(event_simple(&mut event_reader)), | ||
152 | "sect1" => DescriptionNode::Sect1(event_simple(&mut event_reader)), | ||
153 | "internal" => DescriptionNode::Internal(event_simple(&mut event_reader)), | ||
154 | other => { | ||
155 | warn!("Description element not supported: {}", other); | ||
156 | continue; | ||
157 | } | ||
158 | }), | ||
159 | XmlEvent::EndElement { .. } => break, | ||
160 | other => { | ||
161 | warn!("Description event not supported: {:?}", other); | ||
162 | } | ||
163 | } | ||
164 | } | ||
165 | |||
166 | Self { inner } | ||
167 | } | ||
168 | } | ||
169 | |||
170 | impl FromXML for Paragraph { | ||
171 | fn from_xml( | ||
172 | _attributes: Vec<OwnedAttribute>, | ||
173 | mut event_reader: &mut EventReader<impl Read>, | ||
174 | ) -> Self { | ||
175 | let mut inner = Vec::new(); | ||
176 | |||
177 | while let Ok(event) = event_reader.next() { | ||
178 | match event { | ||
179 | XmlEvent::Characters(text) => inner.push(ParagraphNode::Text(text)), | ||
180 | XmlEvent::StartElement { | ||
181 | name, attributes, .. | ||
182 | } => inner.push(match name.local_name.as_str() { | ||
183 | "ref" => ParagraphNode::Ref(Ref::from_xml(attributes, &mut event_reader)), | ||
184 | other => { | ||
185 | warn!("Paragraph element not supported: {}", other); | ||
186 | continue; | ||
187 | } | ||
188 | }), | ||
189 | XmlEvent::EndElement { .. } => break, | ||
190 | other => { | ||
191 | warn!("Paragraph event not supported: {:?}", other); | ||
192 | } | ||
193 | } | ||
194 | } | ||
195 | |||
196 | Self { inner } | ||
197 | } | ||
198 | } | ||
199 | |||
200 | impl FromXML for Ref { | ||
201 | fn from_xml( | ||
202 | attributes: Vec<OwnedAttribute>, | ||
203 | mut event_reader: &mut EventReader<impl Read>, | ||
204 | ) -> Self { | ||
205 | let mut id = None; | ||
206 | let mut kind = None; | ||
207 | |||
208 | for attr in attributes.into_iter() { | ||
209 | match attr.name.local_name.as_str() { | ||
210 | "refid" => id = Some(attr.value), | ||
211 | "kindref" => { | ||
212 | kind = Some(match attr.value.as_str() { | ||
213 | "compound" => RefKind::Compound, | ||
214 | "member" => RefKind::Member, | ||
215 | other => { | ||
216 | warn!("Ref kind not supported: {}", other); | ||
217 | RefKind::Compound | ||
218 | } | ||
219 | }); | ||
220 | } | ||
221 | other => { | ||
222 | warn!("Ref element not supported: {}", other); | ||
223 | } | ||
224 | } | ||
225 | } | ||
226 | |||
227 | let text = event_simple(&mut event_reader); | ||
228 | |||
229 | Ref { | ||
230 | id: id.unwrap(), | ||
231 | kind: kind.unwrap(), | ||
232 | text, | ||
233 | } | ||
234 | } | ||
235 | } | ||
236 | |||
237 | /// Parse a type from a XML Doxygen file | ||
238 | pub(crate) fn parse_type(reader: impl Read) -> Vec<DoxygenType> { | ||
239 | let mut event_reader = EventReader::new(reader); | ||
240 | |||
241 | match event_reader.next().unwrap() { | ||
242 | XmlEvent::StartDocument { .. } => (), | ||
243 | _ => panic!("XML file does not begin with document"), | ||
244 | } | ||
245 | |||
246 | match event_reader.next().unwrap() { | ||
247 | XmlEvent::StartElement { name, .. } => { | ||
248 | if name.local_name != "doxygen" { | ||
249 | panic!("XML file does not start with a 'doxygen' element"); | ||
250 | } | ||
251 | } | ||
252 | _ => panic!("XML file does not start with a 'doxygen' element"), | ||
253 | } | ||
254 | |||
255 | let mut result = Vec::new(); | ||
256 | |||
257 | while let Ok(event) = event_reader.next() { | ||
258 | match dbg!(event) { | ||
259 | XmlEvent::StartElement { | ||
260 | name, attributes, .. | ||
261 | } => { | ||
262 | if name.local_name != "compounddef" { | ||
263 | panic!("'doxygen' is not a sequence of 'compounddef' elements"); | ||
264 | } | ||
265 | |||
266 | result.push(event_compounddef(attributes, &mut event_reader)); | ||
267 | } | ||
268 | XmlEvent::Whitespace(_) => (), | ||
269 | XmlEvent::EndElement { .. } => break, | ||
270 | _ => panic!("'doxygen' is not a sequence of 'compounddef' elements"), | ||
271 | } | ||
272 | } | ||
273 | |||
274 | // Unnecessarily strict? | ||
275 | match event_reader.next().unwrap() { | ||
276 | XmlEvent::EndDocument => (), | ||
277 | _ => panic!("XML file does not end after 'doxygen' element"), | ||
278 | } | ||
279 | |||
280 | dbg!(result) | ||
281 | } | ||
282 | |||
283 | fn event_compounddef( | ||
284 | attributes: Vec<OwnedAttribute>, | ||
285 | mut event_reader: &mut EventReader<impl Read>, | ||
286 | ) -> DoxygenType { | ||
287 | let kind = attributes | ||
288 | .into_iter() | ||
289 | .find_map(|attr| { | ||
290 | if attr.name.local_name == "kind" { | ||
291 | Some(attr.value) | ||
292 | } else { | ||
293 | None | ||
294 | } | ||
295 | }) | ||
296 | .unwrap(); | ||
297 | |||
298 | let mut builder = builders::builder_for(kind); | ||
299 | |||
300 | while let Ok(event) = event_reader.next() { | ||
301 | match dbg!(event) { | ||
302 | XmlEvent::StartElement { | ||
303 | name, attributes, .. | ||
304 | } => match name.local_name.as_str() { | ||
305 | "compoundname" => { | ||
306 | let name = event_simple(&mut event_reader); | ||
307 | builder.name(name); | ||
308 | } | ||
309 | "briefdescription" => { | ||
310 | let brief_description = Description::from_xml(attributes, &mut event_reader); | ||
311 | builder.brief_description(brief_description); | ||
312 | } | ||
313 | "detaileddescription" => { | ||
314 | let detailed_description = Description::from_xml(attributes, &mut event_reader); | ||
315 | builder.detailed_description(detailed_description); | ||
316 | } | ||
317 | "location" => { | ||
318 | event_simple(&mut event_reader); | ||
319 | debug!("Do something?"); | ||
320 | } | ||
321 | "sectiondef" => { | ||
322 | event_section(&mut builder, &mut event_reader); | ||
323 | } | ||
324 | other_name if is_inner_type(other_name) => { | ||
325 | let inner = event_simple(&mut event_reader); | ||
326 | builder.add_inner(inner, other_name.to_string()); | ||
327 | } | ||
328 | _other_name => { | ||
329 | event_ignore(&mut event_reader); | ||
330 | } | ||
331 | }, | ||
332 | XmlEvent::Whitespace(_) => (), | ||
333 | XmlEvent::EndElement { .. } => break, | ||
334 | _ => panic!("Unhandled XML event"), | ||
335 | } | ||
336 | } | ||
337 | |||
338 | builder.build() | ||
339 | } | ||
340 | |||
341 | /// Returns true if the given XML Element is a reference to another inner type. | ||
342 | /// | ||
343 | /// Corresponds to the "refType" XSD type in `compound.xsd` | ||
344 | fn is_inner_type(element_name: &str) -> bool { | ||
345 | match element_name { | ||
346 | "innerdir" | "innerfile" | "innerclass" | "innernamespace" | "innerpage" | "innergroup" => { | ||
347 | true | ||
348 | } | ||
349 | _ => false, | ||
350 | } | ||
351 | } | ||
352 | |||
353 | /// Get the text inside a simple, non recursive XML tag | ||
354 | fn event_simple(event_reader: &mut EventReader<impl Read>) -> String { | ||
355 | let result; | ||
356 | |||
357 | match dbg!(event_reader.next().unwrap()) { | ||
358 | XmlEvent::Characters(tmp_result) => { | ||
359 | result = tmp_result; | ||
360 | } | ||
361 | XmlEvent::EndElement { .. } => return "".to_string(), | ||
362 | _ => panic!("Simple XML event is not so simple"), | ||
363 | } | ||
364 | |||
365 | match dbg!(event_reader.next().unwrap()) { | ||
366 | XmlEvent::EndElement { .. } => (), | ||
367 | _ => panic!("Simple XML event is not so simple"), | ||
368 | } | ||
369 | |||
370 | result | ||
371 | } | ||
372 | |||
373 | fn event_ignore(mut event_reader: &mut EventReader<impl Read>) { | ||
374 | loop { | ||
375 | let event = event_reader.next().unwrap(); | ||
376 | match dbg!(event) { | ||
377 | XmlEvent::StartElement { .. } => event_ignore(&mut event_reader), | ||
378 | XmlEvent::EndElement { .. } => break, | ||
379 | _ => (), | ||
380 | } | ||
381 | } | ||
382 | } | ||
383 | |||
384 | fn event_section( | ||
385 | mut builder: &mut Box<dyn TypeBuilder>, | ||
386 | mut event_reader: &mut EventReader<impl Read>, | ||
387 | ) { | ||
388 | loop { | ||
389 | let event = event_reader.next().unwrap(); | ||
390 | match dbg!(event) { | ||
391 | XmlEvent::StartElement { | ||
392 | name, attributes, .. | ||
393 | } => match name.local_name.as_str() { | ||
394 | "memberdef" => { | ||
395 | let member = event_member(attributes, &mut event_reader); | ||
396 | builder.add_member("bla".to_owned(), member); | ||
397 | } | ||
398 | _ => panic!("Unhandled thing"), | ||
399 | }, | ||
400 | |||
401 | XmlEvent::Whitespace(_) => (), | ||
402 | XmlEvent::EndElement { .. } => break, | ||
403 | _ => panic!("Unknown section XML event"), | ||
404 | } | ||
405 | } | ||
406 | } | ||
407 | |||
408 | fn event_member( | ||
409 | attributes: Vec<OwnedAttribute>, | ||
410 | mut event_reader: &mut EventReader<impl Read>, | ||
411 | ) -> Member { | ||
412 | let kind = attributes | ||
413 | .into_iter() | ||
414 | .find_map(|attr| { | ||
415 | if attr.name.local_name == "kind" { | ||
416 | Some(attr.value) | ||
417 | } else { | ||
418 | None | ||
419 | } | ||
420 | }) | ||
421 | .unwrap(); | ||
422 | |||
423 | match kind.as_str() { | ||
424 | "variable" => event_member_variable(&mut event_reader), | ||
425 | _ => { | ||
426 | event_ignore(&mut event_reader); | ||
427 | Member::Unhandled | ||
428 | } | ||
429 | } | ||
430 | } | ||
431 | |||
432 | fn event_member_variable(mut event_reader: &mut EventReader<impl Read>) -> Member { | ||
433 | let mut member_name = None; | ||
434 | let mut r#type = None; | ||
435 | |||
436 | loop { | ||
437 | let event = event_reader.next().unwrap(); | ||
438 | match dbg!(event) { | ||
439 | XmlEvent::StartElement { name, .. } => match name.local_name.as_str() { | ||
440 | "name" => member_name = Some(event_simple(&mut event_reader)), | ||
441 | "definition" => r#type = Some(event_simple(&mut event_reader)), | ||
442 | _ => event_ignore(&mut event_reader), | ||
443 | }, | ||
444 | XmlEvent::Whitespace(_) => (), | ||
445 | XmlEvent::EndElement { .. } => break, | ||
446 | _ => panic!("Unknown member XML event"), | ||
447 | } | ||
448 | } | ||
449 | |||
450 | Member::Variable { | ||
451 | name: member_name.unwrap(), | ||
452 | r#type: r#type.unwrap(), | ||
453 | } | ||
454 | } | ||
diff --git a/src/doxygen/builders.rs b/src/doxygen/builders.rs new file mode 100644 index 0000000..9b60325 --- /dev/null +++ b/src/doxygen/builders.rs | |||
@@ -0,0 +1,109 @@ | |||
1 | use super::*; | ||
2 | |||
3 | pub(super) fn builder_for(doxygen_kind: String) -> Box<dyn TypeBuilder> { | ||
4 | match doxygen_kind.as_str() { | ||
5 | "namespace" => Box::new(NameSpaceBuilder::default()), | ||
6 | "class" | "struct" => Box::new(ClassBuilder::default()), | ||
7 | _ => Box::new(UnhandledBuilder), | ||
8 | } | ||
9 | } | ||
10 | |||
11 | #[derive(Debug, Default)] | ||
12 | struct NameSpaceBuilder { | ||
13 | name: Option<String>, | ||
14 | members: Vec<String>, | ||
15 | brief_description: Option<Description>, | ||
16 | detailed_description: Option<Description>, | ||
17 | } | ||
18 | |||
19 | impl TypeBuilder for NameSpaceBuilder { | ||
20 | fn build(self: Box<Self>) -> DoxygenType { | ||
21 | let name = self.name.unwrap(); | ||
22 | |||
23 | DoxygenType::NameSpace(NameSpace { | ||
24 | name, | ||
25 | members: self.members, | ||
26 | brief_description: self.brief_description.unwrap(), | ||
27 | detailed_description: self.detailed_description.unwrap(), | ||
28 | }) | ||
29 | } | ||
30 | |||
31 | fn name(&mut self, value: String) { | ||
32 | self.name = Some(value); | ||
33 | } | ||
34 | |||
35 | fn add_inner(&mut self, name: String, kind: String) { | ||
36 | self.members.push(name); | ||
37 | } | ||
38 | |||
39 | fn add_member(&mut self, _section: String, _member: Member) { | ||
40 | panic!("Adding member in namespace?"); | ||
41 | } | ||
42 | |||
43 | fn brief_description(&mut self, description: Description) { | ||
44 | self.brief_description = Some(description); | ||
45 | } | ||
46 | |||
47 | fn detailed_description(&mut self, description: Description) { | ||
48 | self.brief_description = Some(description); | ||
49 | } | ||
50 | } | ||
51 | |||
52 | #[derive(Debug, Default)] | ||
53 | struct UnhandledBuilder; | ||
54 | |||
55 | impl TypeBuilder for UnhandledBuilder { | ||
56 | fn build(self: Box<Self>) -> DoxygenType { | ||
57 | DoxygenType::Unhandled | ||
58 | } | ||
59 | |||
60 | fn name(&mut self, _value: String) {} | ||
61 | |||
62 | fn add_inner(&mut self, _name: String, _kind: String) {} | ||
63 | fn add_member(&mut self, _section: String, _member: Member) {} | ||
64 | fn brief_description(&mut self, _description: Description) {} | ||
65 | fn detailed_description(&mut self, _description: Description) {} | ||
66 | } | ||
67 | |||
68 | #[derive(Debug, Default)] | ||
69 | struct ClassBuilder { | ||
70 | name: Option<String>, | ||
71 | inners: Vec<(String, String)>, | ||
72 | sections: HashMap<String, Vec<Member>>, | ||
73 | brief_description: Option<Description>, | ||
74 | detailed_description: Option<Description>, | ||
75 | } | ||
76 | |||
77 | impl TypeBuilder for ClassBuilder { | ||
78 | fn build(self: Box<Self>) -> DoxygenType { | ||
79 | let name = self.name.unwrap(); | ||
80 | |||
81 | DoxygenType::Class(Class { | ||
82 | name, | ||
83 | inners: self.inners, | ||
84 | sections: self.sections, | ||
85 | brief_description: self.brief_description.unwrap_or(Description::new()), | ||
86 | detailed_description: self.detailed_description.unwrap_or(Description::new()), | ||
87 | }) | ||
88 | } | ||
89 | |||
90 | fn name(&mut self, value: String) { | ||
91 | self.name = Some(value); | ||
92 | } | ||
93 | |||
94 | fn add_inner(&mut self, name: String, kind: String) { | ||
95 | self.inners.push((name, kind)); | ||
96 | } | ||
97 | |||
98 | fn add_member(&mut self, section: String, member: Member) { | ||
99 | self.sections.entry(section).or_default().push(member); | ||
100 | } | ||
101 | |||
102 | fn brief_description(&mut self, description: Description) { | ||
103 | self.brief_description = Some(description); | ||
104 | } | ||
105 | |||
106 | fn detailed_description(&mut self, description: Description) { | ||
107 | self.brief_description = Some(description); | ||
108 | } | ||
109 | } | ||
diff --git a/src/entities.rs b/src/entities.rs new file mode 100644 index 0000000..b7368df --- /dev/null +++ b/src/entities.rs | |||
@@ -0,0 +1,134 @@ | |||
1 | use std::collections::HashMap; | ||
2 | |||
3 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] | ||
4 | pub(crate) struct Usr(pub(crate) String); | ||
5 | |||
6 | #[derive(Debug, Clone)] | ||
7 | pub(crate) struct EntitiesManager { | ||
8 | toplevel_entities: HashMap<Usr, Entity>, | ||
9 | descriptions: HashMap<Usr, Description>, | ||
10 | } | ||
11 | |||
12 | pub(crate) struct EntitiesManagerComponents { | ||
13 | pub(crate) toplevel_entities: HashMap<Usr, Entity>, | ||
14 | pub(crate) descriptions: HashMap<Usr, Description>, | ||
15 | } | ||
16 | |||
17 | impl EntitiesManager { | ||
18 | pub fn new() -> Self { | ||
19 | EntitiesManager { | ||
20 | toplevel_entities: HashMap::new(), | ||
21 | descriptions: HashMap::new(), | ||
22 | } | ||
23 | } | ||
24 | |||
25 | pub fn insert(&mut self, usr: Usr, description: Description) { | ||
26 | info!("Found entity {:?}", description.name); | ||
27 | self.descriptions.insert(usr, description); | ||
28 | } | ||
29 | |||
30 | pub fn insert_toplevel(&mut self, usr: Usr, entity: Entity) { | ||
31 | self.toplevel_entities.insert(usr, entity); | ||
32 | } | ||
33 | |||
34 | pub fn decompose(self) -> EntitiesManagerComponents { | ||
35 | EntitiesManagerComponents { | ||
36 | toplevel_entities: self.toplevel_entities, | ||
37 | descriptions: self.descriptions, | ||
38 | } | ||
39 | } | ||
40 | } | ||
41 | |||
42 | #[derive(Debug, Clone, Default)] | ||
43 | pub(crate) struct Description { | ||
44 | pub(crate) name: String, | ||
45 | pub(crate) brief: String, | ||
46 | pub(crate) detailed: String, | ||
47 | } | ||
48 | |||
49 | impl Description { | ||
50 | pub(crate) fn with_name(name: String) -> Self { | ||
51 | Description { | ||
52 | name, | ||
53 | ..Default::default() | ||
54 | } | ||
55 | } | ||
56 | } | ||
57 | |||
58 | #[derive(Debug, Clone)] | ||
59 | pub(crate) struct Described<T> { | ||
60 | pub(crate) description: Description, | ||
61 | pub(crate) entity: T, | ||
62 | } | ||
63 | |||
64 | #[derive(Debug, Clone)] | ||
65 | pub(crate) enum Entity { | ||
66 | NameSpace(NameSpace), | ||
67 | Variable(Variable), | ||
68 | Function(Function), | ||
69 | Class(Class), | ||
70 | } | ||
71 | |||
72 | impl Entity { | ||
73 | fn kind_name(&self) -> &'static str { | ||
74 | match self { | ||
75 | Entity::NameSpace(_) => "namespace", | ||
76 | Entity::Variable(_) => "variable", | ||
77 | Entity::Function(_) => "function", | ||
78 | Entity::Class(_) => "class", | ||
79 | } | ||
80 | } | ||
81 | } | ||
82 | |||
83 | #[derive(Debug, Clone)] | ||
84 | pub(crate) struct NameSpace { | ||
85 | pub(crate) members: HashMap<Usr, Entity>, | ||
86 | } | ||
87 | |||
88 | impl From<NameSpace> for Entity { | ||
89 | fn from(ns: NameSpace) -> Self { | ||
90 | Entity::NameSpace(ns) | ||
91 | } | ||
92 | } | ||
93 | |||
94 | #[derive(Debug, Clone)] | ||
95 | pub(crate) struct Variable { | ||
96 | pub(crate) r#type: String, | ||
97 | } | ||
98 | |||
99 | impl From<Variable> for Entity { | ||
100 | fn from(var: Variable) -> Self { | ||
101 | Entity::Variable(var) | ||
102 | } | ||
103 | } | ||
104 | |||
105 | #[derive(Debug, Clone)] | ||
106 | pub(crate) struct Function { | ||
107 | pub(crate) arguments: Vec<FunctionArgument>, | ||
108 | pub(crate) return_type: String, | ||
109 | } | ||
110 | |||
111 | impl From<Function> for Entity { | ||
112 | fn from(func: Function) -> Self { | ||
113 | Entity::Function(func) | ||
114 | } | ||
115 | } | ||
116 | |||
117 | #[derive(Debug, Clone)] | ||
118 | pub(crate) struct FunctionArgument { | ||
119 | pub(crate) name: String, | ||
120 | pub(crate) r#type: String, | ||
121 | } | ||
122 | |||
123 | #[derive(Debug, Clone)] | ||
124 | pub(crate) struct Class { | ||
125 | pub(crate) member_types: Vec<Usr>, | ||
126 | pub(crate) member_functions: HashMap<Usr, Function>, | ||
127 | pub(crate) member_variables: HashMap<Usr, Variable>, | ||
128 | } | ||
129 | |||
130 | impl From<Class> for Entity { | ||
131 | fn from(class: Class) -> Self { | ||
132 | Entity::Class(class) | ||
133 | } | ||
134 | } | ||
diff --git a/src/generator.rs b/src/generator.rs new file mode 100644 index 0000000..0ab4511 --- /dev/null +++ b/src/generator.rs | |||
@@ -0,0 +1,209 @@ | |||
1 | use crate::entities::{ | ||
2 | Described, Description, EntitiesManager, EntitiesManagerComponents, Entity, Usr, | ||
3 | }; | ||
4 | |||
5 | use anyhow::{Context, Result}; | ||
6 | use thiserror::Error; | ||
7 | use threadpool::ThreadPool; | ||
8 | |||
9 | use std::collections::HashMap; | ||
10 | use std::path::Path; | ||
11 | use std::sync::{mpsc::channel, Arc}; | ||
12 | |||
13 | pub(crate) fn generate(base_dir: &Path, manager: EntitiesManager) -> Result<()> { | ||
14 | let EntitiesManagerComponents { | ||
15 | toplevel_entities, | ||
16 | descriptions, | ||
17 | } = manager.decompose(); | ||
18 | |||
19 | let md_output_dir = base_dir.join("markdown"); | ||
20 | let html_output_dir = base_dir.join("html"); | ||
21 | |||
22 | std::fs::create_dir_all(&md_output_dir) | ||
23 | .context("Failed to create the markdown output directory")?; | ||
24 | std::fs::create_dir_all(&html_output_dir) | ||
25 | .context("Failed to create the HTML output directory")?; | ||
26 | |||
27 | let pool = ThreadPool::new(num_cpus::get()); | ||
28 | |||
29 | let descriptions = Arc::new(descriptions); | ||
30 | |||
31 | let (tx, rx) = channel::<()>(); | ||
32 | |||
33 | for (usr, entity) in toplevel_entities { | ||
34 | generate_recursively( | ||
35 | usr, | ||
36 | entity, | ||
37 | pool.clone(), | ||
38 | tx.clone(), | ||
39 | descriptions.clone(), | ||
40 | &md_output_dir, | ||
41 | &html_output_dir, | ||
42 | ); | ||
43 | } | ||
44 | |||
45 | drop(tx); | ||
46 | |||
47 | // This is not really idiomatic, but iter returns None when every Sender is destroyed, so just | ||
48 | // by passing around Senders in generate_recursively, we wait for every job. | ||
49 | rx.iter().for_each(drop); | ||
50 | |||
51 | Ok(()) | ||
52 | } | ||
53 | |||
54 | fn generate_recursively( | ||
55 | usr: Usr, | ||
56 | entity: Entity, | ||
57 | pool: ThreadPool, | ||
58 | tx: std::sync::mpsc::Sender<()>, | ||
59 | descriptions: Arc<HashMap<Usr, Description>>, | ||
60 | md_output_dir: impl AsRef<Path>, | ||
61 | html_output_dir: impl AsRef<Path>, | ||
62 | ) { | ||
63 | let descriptions = descriptions.clone(); | ||
64 | let md_output_dir = md_output_dir.as_ref().to_owned(); | ||
65 | let html_output_dir = html_output_dir.as_ref().to_owned(); | ||
66 | |||
67 | let pool2 = pool.clone(); | ||
68 | pool.execute(move || { | ||
69 | trace!("Trying to generate {}", usr.0); | ||
70 | |||
71 | let entity = Described::<Entity> { | ||
72 | entity, | ||
73 | description: descriptions.get(&usr).unwrap().clone(), | ||
74 | }; | ||
75 | |||
76 | let leftovers = generate_single( | ||
77 | &usr, | ||
78 | entity, | ||
79 | &descriptions, | ||
80 | &md_output_dir, | ||
81 | &html_output_dir, | ||
82 | ) | ||
83 | .unwrap(); | ||
84 | |||
85 | for (usr, entity) in leftovers { | ||
86 | let pool = pool2.clone(); | ||
87 | let tx = tx.clone(); | ||
88 | generate_recursively( | ||
89 | usr, | ||
90 | entity, | ||
91 | pool, | ||
92 | tx, | ||
93 | descriptions.clone(), | ||
94 | md_output_dir.clone(), | ||
95 | html_output_dir.clone(), | ||
96 | ); | ||
97 | } | ||
98 | }); | ||
99 | } | ||
100 | |||
101 | fn generate_single( | ||
102 | usr: &Usr, | ||
103 | entity: Described<Entity>, | ||
104 | descriptions: &HashMap<Usr, Description>, | ||
105 | md_output_dir: impl AsRef<Path>, | ||
106 | html_output_dir: impl AsRef<Path>, | ||
107 | ) -> Result<HashMap<Usr, Entity>> { | ||
108 | use std::io::prelude::*; | ||
109 | use std::process::{Command, Stdio}; | ||
110 | |||
111 | let md_output_file = md_output_dir.as_ref().join(&usr.0); | ||
112 | let html_output_file = html_output_dir.as_ref().join(&usr.0); | ||
113 | |||
114 | let mut command = Command::new("pandoc"); | ||
115 | |||
116 | command | ||
117 | .stdin(Stdio::piped()) | ||
118 | .stdout(Stdio::piped()) | ||
119 | .stderr(Stdio::piped()) | ||
120 | .args(&[ | ||
121 | "--from=json", | ||
122 | "--to=markdown", | ||
123 | "--output", | ||
124 | md_output_file | ||
125 | .to_str() | ||
126 | .context("Entity name is not valid UTF-8")?, | ||
127 | ]); | ||
128 | |||
129 | debug!("Launching command: {:?}", command); | ||
130 | |||
131 | let mut pandoc = command | ||
132 | .spawn() | ||
133 | .context("Failed to execute Pandoc command")?; | ||
134 | |||
135 | let (pandoc_ast, leftovers) = entity.into_pandoc(&descriptions); | ||
136 | |||
137 | if log_enabled!(log::Level::Trace) { | ||
138 | let json = | ||
139 | serde_json::to_string(&pandoc_ast).context("Failed to convert Pandoc AST to JSON")?; | ||
140 | trace!("Sending json: {}", json); | ||
141 | write!( | ||
142 | pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), | ||
143 | "{}", | ||
144 | &json, | ||
145 | ) | ||
146 | .context("Failed to convert Pandoc AST to JSON")?; | ||
147 | } else { | ||
148 | serde_json::to_writer( | ||
149 | pandoc.stdin.as_mut().expect("Pandoc stdin not captured"), | ||
150 | &pandoc_ast, | ||
151 | ) | ||
152 | .context("Failed to convert Pandoc AST to JSON")?; | ||
153 | } | ||
154 | |||
155 | let output = pandoc | ||
156 | .wait_with_output() | ||
157 | .expect("Pandoc command wasn't running"); | ||
158 | |||
159 | if !output.status.success() { | ||
160 | Err(CommandError { | ||
161 | status: output.status, | ||
162 | stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), | ||
163 | }) | ||
164 | .context("Pandoc command failed")?; | ||
165 | } | ||
166 | |||
167 | let mut command = Command::new("pandoc"); | ||
168 | command | ||
169 | .stdin(Stdio::piped()) | ||
170 | .stdout(Stdio::piped()) | ||
171 | .stderr(Stdio::piped()) | ||
172 | .args(&[ | ||
173 | "--from=markdown", | ||
174 | "--to=html", | ||
175 | "--css=res/style.css", | ||
176 | "--standalone", | ||
177 | "--self-contained", | ||
178 | md_output_file | ||
179 | .to_str() | ||
180 | .context("Entity name is not valid UTF-8")?, | ||
181 | "--output", | ||
182 | html_output_file | ||
183 | .to_str() | ||
184 | .context("Entity name is not valid UTF-8")?, | ||
185 | ]); | ||
186 | |||
187 | debug!("Launching command: {:?}", command); | ||
188 | |||
189 | let output = command | ||
190 | .output() | ||
191 | .context("Failed to execute Pandoc command")?; | ||
192 | |||
193 | if !output.status.success() { | ||
194 | Err(CommandError { | ||
195 | status: output.status, | ||
196 | stderr: String::from_utf8(output.stderr).expect("Pandoc outputted invalid UTF-8"), | ||
197 | }) | ||
198 | .context("Pandoc command failed")?; | ||
199 | } | ||
200 | |||
201 | Ok(leftovers) | ||
202 | } | ||
203 | |||
204 | #[derive(Debug, Clone, Error)] | ||
205 | #[error("Command returned status {:?} and stderr {:?}", status, stderr)] | ||
206 | struct CommandError { | ||
207 | status: std::process::ExitStatus, | ||
208 | stderr: String, | ||
209 | } | ||
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e1d4752 --- /dev/null +++ b/src/main.rs | |||
@@ -0,0 +1,44 @@ | |||
1 | //mod doxygen; | ||
2 | mod cli; | ||
3 | mod entities; | ||
4 | mod generator; | ||
5 | mod pandoc; | ||
6 | mod parsing; | ||
7 | |||
8 | #[macro_use] | ||
9 | extern crate log; | ||
10 | |||
11 | use cli::Command; | ||
12 | use generator::generate; | ||
13 | use parsing::parse_file; | ||
14 | |||
15 | use anyhow::Result; | ||
16 | use structopt::StructOpt; | ||
17 | |||
18 | fn main() -> Result<()> { | ||
19 | let cli = cli::Cli::from_args(); | ||
20 | pretty_env_logger::formatted_builder() | ||
21 | .filter( | ||
22 | None, | ||
23 | match cli.verbosity { | ||
24 | // Warnings and errors for internal warnings / errors | ||
25 | 0 => log::LevelFilter::Warn, | ||
26 | 1 => log::LevelFilter::Info, | ||
27 | 2 => log::LevelFilter::Debug, | ||
28 | _ => log::LevelFilter::Trace, | ||
29 | }, | ||
30 | ) | ||
31 | .try_init()?; | ||
32 | |||
33 | match cli.command { | ||
34 | Command::Generate { file } => { | ||
35 | let extra_args = cli.extra_arg.iter().flatten().map(AsRef::as_ref).collect(); | ||
36 | let manager = parse_file(file, extra_args); | ||
37 | |||
38 | let base_output_dir = std::path::Path::new("doc"); | ||
39 | generate(&base_output_dir, manager)?; | ||
40 | } | ||
41 | } | ||
42 | |||
43 | Ok(()) | ||
44 | } | ||
diff --git a/src/pandoc.rs b/src/pandoc.rs new file mode 100644 index 0000000..5ca84e7 --- /dev/null +++ b/src/pandoc.rs | |||
@@ -0,0 +1,246 @@ | |||
1 | //mod types; | ||
2 | |||
3 | use crate::entities::*; | ||
4 | |||
5 | use pandoc_types::definition::{Attr, Block, Inline, Meta, MetaValue, Pandoc}; | ||
6 | |||
7 | use std::collections::HashMap; | ||
8 | |||
9 | impl Described<Entity> { | ||
10 | pub fn into_pandoc( | ||
11 | self, | ||
12 | descriptions: &HashMap<Usr, Description>, | ||
13 | ) -> (Pandoc, HashMap<Usr, Entity>) { | ||
14 | let mut meta = Meta::null(); | ||
15 | |||
16 | let title = self.title(); | ||
17 | let mut content_before = self.content_before(&descriptions); | ||
18 | let mut content_after = self.content_after(&descriptions); | ||
19 | let leftovers = self.leftovers(); | ||
20 | |||
21 | meta.0.insert( | ||
22 | "title".to_string(), | ||
23 | MetaValue::MetaString(self.description.name), | ||
24 | ); | ||
25 | |||
26 | let mut content = Vec::new(); | ||
27 | |||
28 | content.push(Block::Header(1, Attr::null(), title)); | ||
29 | |||
30 | content.append(&mut content_before); | ||
31 | |||
32 | if !self.description.detailed.is_empty() { | ||
33 | content.push(Block::Header( | ||
34 | 2, | ||
35 | Attr::null(), | ||
36 | vec![Inline::Str(String::from("Description"))], | ||
37 | )); | ||
38 | |||
39 | content.push(Block::Div( | ||
40 | Attr(String::new(), vec![String::from("doc")], vec![]), | ||
41 | vec![raw_markdown(self.description.detailed)], | ||
42 | )); | ||
43 | } | ||
44 | |||
45 | content.append(&mut content_after); | ||
46 | |||
47 | (Pandoc(meta, content), leftovers) | ||
48 | } | ||
49 | } | ||
50 | |||
51 | // TODO: replace with single function so we can move out, and remove all of those clones | ||
52 | trait PandocDisplay { | ||
53 | fn content_before(&self, _descriptions: &HashMap<Usr, Description>) -> Vec<Block> { | ||
54 | vec![] | ||
55 | } | ||
56 | |||
57 | fn content_after(&self, _descriptions: &HashMap<Usr, Description>) -> Vec<Block> { | ||
58 | vec![] | ||
59 | } | ||
60 | |||
61 | fn leftovers(&self) -> HashMap<Usr, Entity> { | ||
62 | HashMap::new() | ||
63 | } | ||
64 | } | ||
65 | |||
66 | impl Described<Entity> { | ||
67 | fn title(&self) -> Vec<Inline> { | ||
68 | match &self.entity { | ||
69 | Entity::Variable(variable) => vec![Inline::Code( | ||
70 | Attr(String::new(), vec![String::from("cpp")], vec![]), | ||
71 | variable.r#type.clone() + " " + &self.description.name, | ||
72 | )], | ||
73 | _ => vec![Inline::Code(Attr::null(), self.description.name.clone())], | ||
74 | } | ||
75 | } | ||
76 | } | ||
77 | |||
78 | impl PandocDisplay for Described<Entity> { | ||
79 | fn content_before(&self, descriptions: &HashMap<Usr, Description>) -> Vec<Block> { | ||
80 | match &self.entity { | ||
81 | Entity::NameSpace(ns) => ns.content_before(descriptions), | ||
82 | Entity::Variable(var) => var.content_before(descriptions), | ||
83 | Entity::Function(func) => func.content_before(descriptions), | ||
84 | Entity::Class(class) => class.content_before(descriptions), | ||
85 | } | ||
86 | } | ||
87 | |||
88 | fn content_after(&self, descriptions: &HashMap<Usr, Description>) -> Vec<Block> { | ||
89 | match &self.entity { | ||
90 | Entity::NameSpace(ns) => ns.content_after(descriptions), | ||
91 | Entity::Variable(var) => var.content_after(descriptions), | ||
92 | Entity::Function(func) => func.content_after(descriptions), | ||
93 | Entity::Class(class) => class.content_after(descriptions), | ||
94 | } | ||
95 | } | ||
96 | |||
97 | fn leftovers(&self) -> HashMap<Usr, Entity> { | ||
98 | match &self.entity { | ||
99 | Entity::NameSpace(ns) => ns.leftovers(), | ||
100 | Entity::Variable(var) => var.leftovers(), | ||
101 | Entity::Function(func) => func.leftovers(), | ||
102 | Entity::Class(class) => class.leftovers(), | ||
103 | } | ||
104 | } | ||
105 | } | ||
106 | |||
107 | impl PandocDisplay for NameSpace { | ||
108 | fn content_after(&self, descriptions: &HashMap<Usr, Description>) -> Vec<Block> { | ||
109 | let mut content = Vec::new(); | ||
110 | |||
111 | content.push(Block::Header( | ||
112 | 2, | ||
113 | Attr::null(), | ||
114 | vec![Inline::Str("Members".to_string())], | ||
115 | )); | ||
116 | |||
117 | if let Some(member_list) = member_list(self.members.keys(), descriptions) { | ||
118 | content.push(member_list); | ||
119 | } else { | ||
120 | content.push(str_block(String::from("None"))); | ||
121 | } | ||
122 | |||
123 | content | ||
124 | } | ||
125 | |||
126 | fn leftovers(&self) -> HashMap<Usr, Entity> { | ||
127 | self.members.clone() | ||
128 | } | ||
129 | } | ||
130 | |||
131 | impl PandocDisplay for Class { | ||
132 | fn content_after(&self, descriptions: &HashMap<Usr, Description>) -> Vec<Block> { | ||
133 | let mut content = Vec::new(); | ||
134 | |||
135 | if let Some(member_types) = member_list(&self.member_types, descriptions) { | ||
136 | content.push(Block::Header( | ||
137 | 2, | ||
138 | Attr::null(), | ||
139 | vec![Inline::Str("Member Types".to_string())], | ||
140 | )); | ||
141 | |||
142 | content.push(member_types); | ||
143 | } | ||
144 | |||
145 | if let Some(member_functions) = member_list(self.member_functions.keys(), descriptions) { | ||
146 | content.push(Block::Header( | ||
147 | 2, | ||
148 | Attr::null(), | ||
149 | vec![Inline::Str("Member Functions".to_string())], | ||
150 | )); | ||
151 | |||
152 | content.push(member_functions); | ||
153 | } | ||
154 | |||
155 | if let Some(member_variables) = member_list(self.member_variables.keys(), descriptions) { | ||
156 | content.push(Block::Header( | ||
157 | 2, | ||
158 | Attr::null(), | ||
159 | vec![Inline::Str("Member Variables".to_string())], | ||
160 | )); | ||
161 | |||
162 | content.push(member_variables); | ||
163 | } | ||
164 | |||
165 | content | ||
166 | } | ||
167 | |||
168 | fn leftovers(&self) -> HashMap<Usr, Entity> { | ||
169 | self.member_functions | ||
170 | .iter() | ||
171 | .map(|(usr, func)| (usr.clone(), Entity::from(func.clone()))) | ||
172 | .chain( | ||
173 | self.member_variables | ||
174 | .iter() | ||
175 | .map(|(usr, var)| (usr.clone(), Entity::from(var.clone()))), | ||
176 | ) | ||
177 | .collect() | ||
178 | } | ||
179 | } | ||
180 | |||
181 | impl PandocDisplay for Variable {} | ||
182 | |||
183 | impl PandocDisplay for Function {} | ||
184 | |||
185 | fn str_block(content: String) -> Block { | ||
186 | Block::Plain(vec![Inline::Str(content)]) | ||
187 | } | ||
188 | |||
189 | fn entity_link(usr: &Usr, name: String) -> Inline { | ||
190 | use pandoc_types::definition::Target; | ||
191 | use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS}; | ||
192 | |||
193 | use std::iter::once; | ||
194 | |||
195 | // https://url.spec.whatwg.org/#fragment-percent-encode-set | ||
196 | const FRAGMENT: &AsciiSet = &CONTROLS | ||
197 | .add(b' ') | ||
198 | .add(b'"') | ||
199 | .add(b'<') | ||
200 | .add(b'>') | ||
201 | .add(b'`') | ||
202 | .add(b'#') | ||
203 | .add(b'?') | ||
204 | .add(b'{') | ||
205 | .add(b'}'); | ||
206 | |||
207 | Inline::Link( | ||
208 | Attr::null(), | ||
209 | vec![Inline::Code(Attr::null(), name)], | ||
210 | Target( | ||
211 | once("./") | ||
212 | .chain(utf8_percent_encode(&usr.0, FRAGMENT)) | ||
213 | .collect(), | ||
214 | String::new(), | ||
215 | ), | ||
216 | ) | ||
217 | } | ||
218 | |||
219 | fn raw_markdown(text: String) -> Block { | ||
220 | use pandoc_types::definition::Format; | ||
221 | Block::RawBlock(Format(String::from("markdown")), text) | ||
222 | } | ||
223 | |||
224 | fn member_list<'a>( | ||
225 | members: impl IntoIterator<Item = &'a Usr>, | ||
226 | descriptions: &HashMap<Usr, Description>, | ||
227 | ) -> Option<Block> { | ||
228 | let definitions: Vec<(Vec<Inline>, Vec<Vec<Block>>)> = members | ||
229 | .into_iter() | ||
230 | .filter_map(|usr| { | ||
231 | let name = &descriptions.get(usr)?.name; | ||
232 | Some(( | ||
233 | vec![entity_link(usr, name.clone())], | ||
234 | vec![vec![str_block( | ||
235 | descriptions.get(usr).unwrap().brief.clone(), | ||
236 | )]], | ||
237 | )) | ||
238 | }) | ||
239 | .collect(); | ||
240 | |||
241 | if definitions.is_empty() { | ||
242 | None | ||
243 | } else { | ||
244 | Some(Block::DefinitionList(definitions)) | ||
245 | } | ||
246 | } | ||
diff --git a/src/pandoc/types.rs b/src/pandoc/types.rs new file mode 100644 index 0000000..dc5be64 --- /dev/null +++ b/src/pandoc/types.rs | |||
@@ -0,0 +1,39 @@ | |||
1 | use crate::pandoc::{Block, Inline}; | ||
2 | |||
3 | #[derive(Debug, Clone)] | ||
4 | pub(super) struct Class { | ||
5 | inners: Vec<Inner>, | ||
6 | } | ||
7 | |||
8 | #[derive(Debug, Clone)] | ||
9 | struct Inner { | ||
10 | kind: InnerKind, | ||
11 | name: String, | ||
12 | //refid: String | ||
13 | } | ||
14 | |||
15 | #[derive(Debug, Clone)] | ||
16 | enum InnerKind { | ||
17 | Class, | ||
18 | Enum, | ||
19 | } | ||
20 | |||
21 | impl std::fmt::Display for InnerKind { | ||
22 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { | ||
23 | match self { | ||
24 | InnerKind::Class => write!(f, "class"), | ||
25 | InnerKind::Enum => write!(f, "enum"), | ||
26 | } | ||
27 | } | ||
28 | } | ||
29 | |||
30 | impl From<Inner> for (Vec<Inline>, Vec<Vec<Block>>) { | ||
31 | fn from(inner: Inner) -> (Vec<Inline>, Vec<Vec<Block>>) { | ||
32 | ( | ||
33 | vec![Inline::Str(inner.name)], | ||
34 | vec![vec![Block::Plain(vec![Inline::Str( | ||
35 | inner.kind.to_string(), | ||
36 | )])]], | ||
37 | ) | ||
38 | } | ||
39 | } | ||
diff --git a/src/parsing.rs b/src/parsing.rs new file mode 100644 index 0000000..137b89d --- /dev/null +++ b/src/parsing.rs | |||
@@ -0,0 +1,457 @@ | |||
1 | use crate::entities::*; | ||
2 | |||
3 | use clang::{Clang, Index}; | ||
4 | use codemap::CodeMap; | ||
5 | |||
6 | use std::collections::HashMap; | ||
7 | use std::path::{Path, PathBuf}; | ||
8 | |||
9 | pub(crate) fn parse_file<T>(path: T, mut extra_args: Vec<&str>) -> EntitiesManager | ||
10 | where | ||
11 | T: Into<PathBuf>, | ||
12 | T: AsRef<Path>, | ||
13 | T: ToString, | ||
14 | { | ||
15 | let mut codemap = CodeMap::new(); | ||
16 | let file_map = codemap.add_file(path.to_string(), std::fs::read_to_string(&path).unwrap()); | ||
17 | let file_span = file_map.span; | ||
18 | |||
19 | let clang = Clang::new().unwrap(); | ||
20 | let index = Index::new(&clang, true, false); | ||
21 | let mut parser = index.parser(path); | ||
22 | parser.skip_function_bodies(true); | ||
23 | |||
24 | extra_args.push("-Werror=documentation"); | ||
25 | parser.arguments(&extra_args); | ||
26 | |||
27 | if log_enabled!(log::Level::Debug) { | ||
28 | for extra_arg in extra_args { | ||
29 | debug!("Extra LibClang argument: {:?}", extra_arg); | ||
30 | } | ||
31 | } | ||
32 | |||
33 | let trans_unit = parser.parse().unwrap(); | ||
34 | |||
35 | let mut manager = EntitiesManager::new(); | ||
36 | |||
37 | trans_unit.get_entity().visit_children(|entity, _parent| { | ||
38 | if entity.is_in_system_header() { | ||
39 | trace!( | ||
40 | "Entity is in system header, skipping: USR = {:?}", | ||
41 | entity.get_display_name() | ||
42 | ); | ||
43 | return clang::EntityVisitResult::Continue; | ||
44 | } | ||
45 | |||
46 | // TODO: wrap this callback in another function so that we can use the | ||
47 | // "?" operator instead of all these `match`es | ||
48 | let usr = match entity.get_usr() { | ||
49 | Some(usr) => usr, | ||
50 | None => return clang::EntityVisitResult::Continue, | ||
51 | }; | ||
52 | trace!("Entity with USR = {:?}", usr); | ||
53 | |||
54 | let name = match entity.get_name() { | ||
55 | Some(name) => name, | ||
56 | None => return clang::EntityVisitResult::Continue, | ||
57 | }; | ||
58 | |||
59 | debug!("Parsing toplevel entity with name = {:?}", name); | ||
60 | |||
61 | let entity = entity.into_poseidoc_entity(&mut manager); | ||
62 | |||
63 | manager.insert_toplevel(usr.into(), entity); | ||
64 | |||
65 | clang::EntityVisitResult::Continue | ||
66 | }); | ||
67 | |||
68 | use codemap_diagnostic::{ColorConfig, Emitter}; | ||
69 | |||
70 | let mut emitter = Emitter::stderr(ColorConfig::Auto, Some(&codemap)); | ||
71 | |||
72 | for diagnostic in trans_unit.get_diagnostics().iter() { | ||
73 | let main_diag = match clang_diag_to_codemap_diag(&diagnostic, &file_span) { | ||
74 | Some(diag) => diag, | ||
75 | None => continue, | ||
76 | }; | ||
77 | |||
78 | let sub_diags = diagnostic | ||
79 | .get_children() | ||
80 | .into_iter() | ||
81 | .filter_map(|diagnostic| clang_diag_to_codemap_diag(&diagnostic, &file_span)); | ||
82 | |||
83 | let fix_it_diags = diagnostic | ||
84 | .get_fix_its() | ||
85 | .into_iter() | ||
86 | .map(|fix_it| clang_fix_it_to_codemap_diag(&fix_it, &file_span)); | ||
87 | |||
88 | emitter.emit( | ||
89 | &std::iter::once(main_diag) | ||
90 | .chain(sub_diags) | ||
91 | .chain(fix_it_diags) | ||
92 | .collect::<Vec<_>>(), | ||
93 | ); | ||
94 | } | ||
95 | |||
96 | manager | ||
97 | } | ||
98 | |||
99 | fn clang_diag_to_codemap_diag( | ||
100 | diagnostic: &clang::diagnostic::Diagnostic, | ||
101 | file_span: &codemap::Span, | ||
102 | ) -> Option<codemap_diagnostic::Diagnostic> { | ||
103 | use codemap_diagnostic::{Diagnostic, Level, SpanLabel, SpanStyle}; | ||
104 | |||
105 | let ranges = diagnostic.get_ranges(); | ||
106 | |||
107 | let (begin, end) = if ranges.is_empty() { | ||
108 | let location = diagnostic.get_location(); | ||
109 | |||
110 | ( | ||
111 | location.get_file_location().offset as u64, | ||
112 | location.get_file_location().offset as u64, | ||
113 | ) | ||
114 | } else { | ||
115 | let range = diagnostic.get_ranges()[0]; | ||
116 | if !range.is_in_main_file() { | ||
117 | warn!("Skipping diagnostic: {:?}", diagnostic); | ||
118 | return None; | ||
119 | } | ||
120 | ( | ||
121 | range.get_start().get_file_location().offset as u64, | ||
122 | range.get_end().get_file_location().offset as u64, | ||
123 | ) | ||
124 | }; | ||
125 | |||
126 | let label = SpanLabel { | ||
127 | span: file_span.subspan(begin, end), | ||
128 | label: None, | ||
129 | style: SpanStyle::Primary, | ||
130 | }; | ||
131 | |||
132 | Some(Diagnostic { | ||
133 | level: match diagnostic.get_severity() { | ||
134 | clang::diagnostic::Severity::Ignored => return None, | ||
135 | clang::diagnostic::Severity::Note => Level::Note, | ||
136 | clang::diagnostic::Severity::Warning => Level::Warning, | ||
137 | clang::diagnostic::Severity::Error => Level::Error, | ||
138 | clang::diagnostic::Severity::Fatal => Level::Error, | ||
139 | }, | ||
140 | message: diagnostic.get_text(), | ||
141 | //code: Some("-Werror=documentation".to_string()), | ||
142 | code: None, | ||
143 | spans: vec![label], | ||
144 | }) | ||
145 | } | ||
146 | |||
147 | fn clang_fix_it_to_codemap_diag( | ||
148 | fix_it: &clang::diagnostic::FixIt, | ||
149 | file_span: &codemap::Span, | ||
150 | ) -> codemap_diagnostic::Diagnostic { | ||
151 | use clang::diagnostic::FixIt; | ||
152 | use codemap_diagnostic::{Diagnostic, Level, SpanLabel, SpanStyle}; | ||
153 | |||
154 | let label = match fix_it { | ||
155 | FixIt::Deletion(range) => { | ||
156 | let begin = range.get_start().get_file_location().offset as u64; | ||
157 | let end = range.get_end().get_file_location().offset as u64; | ||
158 | |||
159 | SpanLabel { | ||
160 | span: file_span.subspan(begin, end), | ||
161 | label: Some(String::from("Try deleting this")), | ||
162 | style: SpanStyle::Primary, | ||
163 | } | ||
164 | } | ||
165 | FixIt::Insertion(range, text) => { | ||
166 | let location = range.get_file_location().offset as u64; | ||
167 | |||
168 | SpanLabel { | ||
169 | span: file_span.subspan(location, location), | ||
170 | label: Some(format!("Try insterting {:?}", text)), | ||
171 | style: SpanStyle::Primary, | ||
172 | } | ||
173 | } | ||
174 | FixIt::Replacement(range, text) => { | ||
175 | let begin = range.get_start().get_file_location().offset as u64; | ||
176 | let end = range.get_end().get_file_location().offset as u64; | ||
177 | |||
178 | SpanLabel { | ||
179 | span: file_span.subspan(begin, end), | ||
180 | label: Some(format!("Try replacing this with {:?}", text)), | ||
181 | style: SpanStyle::Primary, | ||
182 | } | ||
183 | } | ||
184 | }; | ||
185 | |||
186 | Diagnostic { | ||
187 | level: Level::Help, | ||
188 | message: String::from("Possible fix"), | ||
189 | //code: Some("-Werror=documentation".to_string()), | ||
190 | code: None, | ||
191 | spans: vec![label], | ||
192 | } | ||
193 | } | ||
194 | |||
195 | pub(crate) struct Comment(String); | ||
196 | |||
197 | impl Comment { | ||
198 | pub fn from_raw(raw: String) -> Self { | ||
199 | #[derive(Debug)] | ||
200 | enum CommentStyle { | ||
201 | // Comments of type `/**` or `/*!` | ||
202 | Starred, | ||
203 | // Comments of type `///` | ||
204 | SingleLine, | ||
205 | } | ||
206 | |||
207 | let mut chars = raw.chars(); | ||
208 | let style = match &chars.as_str()[..3] { | ||
209 | "/*!" | "/**" => CommentStyle::Starred, | ||
210 | "///" => CommentStyle::SingleLine, | ||
211 | _ => panic!("Comment is empty or doesn't start with either `///`, `/**`, or `/*!`"), | ||
212 | }; | ||
213 | |||
214 | chars.nth(2); | ||
215 | |||
216 | let mut result = String::new(); | ||
217 | |||
218 | 'parse_loop: loop { | ||
219 | let maybe_space = chars.next(); | ||
220 | let mut empty_line = false; | ||
221 | match maybe_space { | ||
222 | // TODO: Warn on empty comments | ||
223 | None => break, | ||
224 | Some(' ') => {} | ||
225 | Some('\n') => { | ||
226 | empty_line = true; | ||
227 | result.push('\n'); | ||
228 | } | ||
229 | Some(ch) => result.push(ch), | ||
230 | } | ||
231 | |||
232 | if !empty_line { | ||
233 | let rest = chars.as_str(); | ||
234 | match rest.find('\n') { | ||
235 | None => { | ||
236 | result.push_str(rest); | ||
237 | break; | ||
238 | } | ||
239 | Some(position) => { | ||
240 | result.push_str(&rest[..position + 1]); | ||
241 | chars.nth(position); | ||
242 | } | ||
243 | } | ||
244 | } | ||
245 | |||
246 | // Beginning of the line | ||
247 | let first_non_ws_ch = 'ws_loop: loop { | ||
248 | let maybe_whitespace = chars.next(); | ||
249 | match maybe_whitespace { | ||
250 | None => break 'parse_loop, | ||
251 | Some(ch) if ch.is_whitespace() => continue, | ||
252 | Some(ch) => break 'ws_loop ch, | ||
253 | } | ||
254 | }; | ||
255 | |||
256 | match style { | ||
257 | CommentStyle::Starred if first_non_ws_ch == '*' => { | ||
258 | if &chars.as_str()[..1] == "/" { | ||
259 | break; | ||
260 | } | ||
261 | } | ||
262 | CommentStyle::Starred => result.push(first_non_ws_ch), | ||
263 | CommentStyle::SingleLine => { | ||
264 | assert!(first_non_ws_ch == '/'); | ||
265 | let rest = chars.as_str(); | ||
266 | if &rest[..2] == "//" { | ||
267 | chars.nth(1); | ||
268 | } else if &rest[..1] == "/" { | ||
269 | chars.nth(0); | ||
270 | } else { | ||
271 | panic!("Could not parse comment"); | ||
272 | } | ||
273 | } | ||
274 | } | ||
275 | } | ||
276 | |||
277 | Self(result) | ||
278 | } | ||
279 | } | ||
280 | |||
281 | impl From<clang::Usr> for Usr { | ||
282 | fn from(usr: clang::Usr) -> Self { | ||
283 | Self(usr.0) | ||
284 | } | ||
285 | } | ||
286 | |||
287 | trait FromClangEntity { | ||
288 | /// Is responsible for inserting its documentation into the entities manager | ||
289 | fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self; | ||
290 | } | ||
291 | |||
292 | trait IntoPoseidocEntity<T> { | ||
293 | /// Useful for the `clang_entity.into_poseidoc_entity(&mut manager)` syntax. Implement | ||
294 | /// `FromClangEntity` instead. | ||
295 | fn into_poseidoc_entity(self, manager: &mut EntitiesManager) -> T; | ||
296 | } | ||
297 | |||
298 | impl<T> IntoPoseidocEntity<T> for clang::Entity<'_> | ||
299 | where | ||
300 | T: FromClangEntity, | ||
301 | { | ||
302 | fn into_poseidoc_entity(self, manager: &mut EntitiesManager) -> T { | ||
303 | T::from_clang_entity(self, manager) | ||
304 | } | ||
305 | } | ||
306 | |||
307 | fn get_description(entity: &clang::Entity) -> Description { | ||
308 | let name = entity.get_display_name().unwrap(); | ||
309 | |||
310 | if let (Some(brief), Some(comment)) = (entity.get_comment_brief(), entity.get_comment()) { | ||
311 | Description { | ||
312 | name, | ||
313 | brief, | ||
314 | detailed: Comment::from_raw(comment).0, | ||
315 | } | ||
316 | } else { | ||
317 | Description::with_name(name) | ||
318 | } | ||
319 | } | ||
320 | |||
321 | impl FromClangEntity for Entity { | ||
322 | fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { | ||
323 | use clang::EntityKind; | ||
324 | |||
325 | match entity.get_kind() { | ||
326 | EntityKind::Namespace => Self::NameSpace(entity.into_poseidoc_entity(manager)), | ||
327 | EntityKind::FieldDecl | EntityKind::VarDecl => { | ||
328 | Self::Variable(entity.into_poseidoc_entity(manager)) | ||
329 | } | ||
330 | EntityKind::FunctionDecl | EntityKind::Method | EntityKind::FunctionTemplate => { | ||
331 | Self::Function(entity.into_poseidoc_entity(manager)) | ||
332 | } | ||
333 | EntityKind::ClassDecl | EntityKind::StructDecl | EntityKind::ClassTemplate => { | ||
334 | Self::Class(entity.into_poseidoc_entity(manager)) | ||
335 | } | ||
336 | _ => panic!("Unsupported entity: {:?}", entity), | ||
337 | } | ||
338 | } | ||
339 | } | ||
340 | |||
341 | impl FromClangEntity for NameSpace { | ||
342 | fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { | ||
343 | match entity.get_kind() { | ||
344 | clang::EntityKind::Namespace => {} | ||
345 | _ => panic!("Trying to parse a non-variable into a variable"), | ||
346 | } | ||
347 | debug!("Parsing NameSpace"); | ||
348 | |||
349 | let members = entity | ||
350 | .get_children() | ||
351 | .into_iter() | ||
352 | .map(|child| { | ||
353 | let usr = child.get_usr().unwrap().into(); | ||
354 | trace!("Namespace has member: {:?}", usr); | ||
355 | (usr, child.into_poseidoc_entity(manager)) | ||
356 | }) | ||
357 | .collect(); | ||
358 | |||
359 | manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); | ||
360 | |||
361 | NameSpace { members } | ||
362 | } | ||
363 | } | ||
364 | |||
365 | impl FromClangEntity for Variable { | ||
366 | fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { | ||
367 | match entity.get_kind() { | ||
368 | clang::EntityKind::VarDecl | clang::EntityKind::FieldDecl => {} | ||
369 | _ => panic!("Trying to parse a non-variable into a variable"), | ||
370 | } | ||
371 | debug!("Parsing Variable"); | ||
372 | |||
373 | let r#type = entity.get_type().unwrap().get_display_name(); | ||
374 | trace!("Variable has type: {:?}", r#type); | ||
375 | |||
376 | manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); | ||
377 | |||
378 | Variable { r#type } | ||
379 | } | ||
380 | } | ||
381 | |||
382 | impl FromClangEntity for Function { | ||
383 | fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { | ||
384 | match entity.get_kind() { | ||
385 | clang::EntityKind::Method | ||
386 | | clang::EntityKind::FunctionDecl | ||
387 | | clang::EntityKind::FunctionTemplate => {} | ||
388 | _ => panic!("Trying to parse a non-function into a function"), | ||
389 | } | ||
390 | debug!("Parsing Function"); | ||
391 | |||
392 | let return_type = entity.get_result_type().unwrap().get_display_name(); | ||
393 | trace!("Function has return type: {:?}", return_type); | ||
394 | let arguments = entity | ||
395 | .get_arguments() | ||
396 | .unwrap() | ||
397 | .into_iter() | ||
398 | .map(|arg| { | ||
399 | let name = arg.get_display_name().unwrap(); | ||
400 | let r#type = arg.get_type().unwrap().get_display_name(); | ||
401 | trace!("Function has argument {:?} of type {:?}", name, r#type); | ||
402 | FunctionArgument { name, r#type } | ||
403 | }) | ||
404 | .collect(); | ||
405 | |||
406 | manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); | ||
407 | |||
408 | Function { | ||
409 | arguments, | ||
410 | return_type, | ||
411 | } | ||
412 | } | ||
413 | } | ||
414 | |||
415 | impl FromClangEntity for Class { | ||
416 | fn from_clang_entity(entity: clang::Entity, manager: &mut EntitiesManager) -> Self { | ||
417 | match entity.get_kind() { | ||
418 | clang::EntityKind::ClassDecl | ||
419 | | clang::EntityKind::StructDecl | ||
420 | | clang::EntityKind::ClassTemplate => {} | ||
421 | _ => panic!("Trying to parse a non-class into a class"), | ||
422 | } | ||
423 | debug!("Parsing Class"); | ||
424 | |||
425 | let mut member_types = Vec::new(); | ||
426 | let mut member_functions = HashMap::new(); | ||
427 | let mut member_variables = HashMap::new(); | ||
428 | |||
429 | for child in entity.get_children() { | ||
430 | trace!("Class has child: {:?}", child); | ||
431 | |||
432 | let child_usr = child.get_usr().unwrap().into(); | ||
433 | |||
434 | use clang::EntityKind; | ||
435 | match child.get_kind() { | ||
436 | EntityKind::ClassDecl | EntityKind::StructDecl | EntityKind::TypeAliasDecl => { | ||
437 | member_types.push(child_usr); | ||
438 | } | ||
439 | EntityKind::Method => { | ||
440 | member_functions.insert(child_usr, child.into_poseidoc_entity(manager)); | ||
441 | } | ||
442 | EntityKind::FieldDecl => { | ||
443 | member_variables.insert(child_usr, child.into_poseidoc_entity(manager)); | ||
444 | } | ||
445 | _ => trace!("Skipping child"), | ||
446 | } | ||
447 | } | ||
448 | |||
449 | manager.insert(entity.get_usr().unwrap().into(), get_description(&entity)); | ||
450 | |||
451 | Class { | ||
452 | member_types, | ||
453 | member_functions, | ||
454 | member_variables, | ||
455 | } | ||
456 | } | ||
457 | } | ||