playdate_bindgen/gen/docs/
parser.rs

1//! Playdate SDK C-API docs parser
2//!
3//! Util that parses `Inside Playdate with C.html` file and produces map with
4//! keys like `sound.effect.bitCrusher.setAmountModulator`
5//! and values with doc in markdown format.
6//!
7//! Used for generating doc-comments for bindings.
8
9
10use std::io::{Error as IoError, ErrorKind};
11use std::borrow::BorrowMut;
12use std::collections::HashMap;
13use html2md::NodeData;
14use html2md::RcDom;
15use html2md::StructuredPrinter;
16use html2md::TagHandler;
17use html5ever::parse_document;
18use html5ever::tendril::TendrilSink;
19use html5ever::tree_builder::TreeSink;
20use markup5ever_rcdom::Handle;
21use markup5ever_rcdom::SerializableHandle;
22use utils::toolchain::sdk::Sdk;
23
24use crate::Result;
25use super::DocsMap;
26
27
28pub fn parse(sdk: &Sdk) -> Result<DocsMap> {
29	let path = sdk.path().join("Inside Playdate with C.html");
30	parse_file(&path).map_err(Into::into)
31}
32
33
34pub fn parse_file(path: &std::path::Path) -> Result<DocsMap, IoError> {
35	if !path.try_exists()? {
36		return Err(IoError::new(ErrorKind::NotFound, path.display().to_string()));
37	}
38
39	let mut results = DocsMap::new();
40	let dom = parse_document(RcDom::default(), Default::default()).from_utf8()
41	                                                              .from_file(path)?
42	                                                              .finish();
43	if !dom.errors.is_empty() {
44		eprintln!("errors: {:#?}", dom.errors);
45	}
46
47	walk(&dom.document, &mut results);
48
49	#[cfg(feature = "log")]
50	for (k, _) in &results {
51		println!("Doc found for {k}");
52	}
53
54	Ok(results)
55}
56
57
58// TODO: optimize, use Cow instead of String.
59fn walk(handle: &Handle, results: &mut DocsMap) {
60	let node = handle;
61	let mut found = None;
62	match node.data {
63		NodeData::Element { ref name, ref attrs, .. } => {
64			found = if name.local == *"div" {
65				let attrs = attrs.borrow();
66				let attr = attrs.iter()
67				                .find(|attr| attr.name.local == *"id" && attr.value.starts_with("f-"));
68				attr.map(|attr| {
69					    attr.value
70					        .strip_prefix("f-")
71					        .expect("prefix 'f-' must be there")
72					        .to_string()
73				    })
74			} else {
75				None
76			};
77
78			if let Some(_key) = found.as_ref() {
79				// Changing: this-div . div_class="title" is a cpp-code fn-path+definition
80				//                  to `<code data-lang="c">`
81				// TODO: also fix links like `<a href="#f-sound.source">SoundSource</a>`
82				let mut children = node.children.borrow_mut();
83				let title = children.iter_mut().find(|child| {
84					                               match &child.data {
85						                               NodeData::Element { name, attrs, .. } => {
86						                                  name.local == *"div" &&
87						                                  attrs.borrow()
88						                                       .iter()
89						                                       .find(|attr| {
90							                                       attr.name.local == *"class" &&
91							                                       attr.value.contains("title")
92						                                       })
93						                                       .is_some()
94					                                  },
95					                                  _ => false,
96					                               }
97				                               });
98				if let Some(title) = title {
99					let mut data = {
100						match &title.data {
101							NodeData::Element { name,
102							                    attrs,
103							                    template_contents,
104							                    mathml_annotation_xml_integration_point, } => {
105								let mut code = name.clone();
106								code.borrow_mut().local = html5ever::ATOM_LOCALNAME__63_6F_64_65;
107								NodeData::Element { name: code,
108								                    attrs: attrs.clone(),
109								                    template_contents: template_contents.clone(),
110								                    mathml_annotation_xml_integration_point:
111									                    *mathml_annotation_xml_integration_point }.into()
112							},
113							_ => None,
114						}
115					};
116
117					if let Some(data) = data.take() {
118						unsafe {
119							std::rc::Rc::get_mut_unchecked(title).data = data;
120						}
121					}
122				}
123			}
124		},
125
126		_ => {},
127	}
128
129	for child in node.children.borrow().iter().filter(|child| {
130		                                          match child.data {
131			                                          NodeData::Text { .. } | NodeData::Element { .. } => true,
132		                                             _ => false,
133		                                          }
134	                                          })
135	{
136		walk(child, results);
137	}
138
139	if let Some(key) = found {
140		let document: SerializableHandle = node.clone().into();
141		let mut render = Vec::new();
142		html5ever::serialize(&mut render, &document, Default::default()).ok()
143		                                                                .expect("serialization failed");
144		let html = std::str::from_utf8(&render).unwrap();
145
146		use html2md::TagHandlerFactory;
147		struct PreAsIsTagFactory;
148		impl TagHandlerFactory for PreAsIsTagFactory {
149			fn instantiate(&self) -> Box<dyn TagHandler> {
150				Box::new(CodeHandler { lang: "cpp",
151				                       ..Default::default() })
152			}
153		}
154		// TODO:
155		let mut tag_factory: HashMap<String, Box<dyn TagHandlerFactory>> = HashMap::new();
156		tag_factory.insert(String::from("pre"), Box::new(PreAsIsTagFactory));
157		let md = html2md::parse_html_custom(html, &tag_factory);
158
159		results.insert(key, md);
160	}
161}
162
163
164#[derive(Default)]
165/// Produces markdown code-block and set lang for each <pre>: ```cpp...```.
166/// It needed to do not produce broken doctest in comments.
167pub struct CodeHandler {
168	/// Default lang for `pre` tags.
169	lang: &'static str,
170	code_type: String,
171}
172
173impl CodeHandler {
174	/// Used in both starting and finishing handling
175	fn do_handle(&mut self, printer: &mut StructuredPrinter, start: bool) {
176		let immediate_parent = printer.parent_chain.last().unwrap().to_owned();
177		if self.code_type == "code" && immediate_parent == "pre" {
178			// we are already in "code" mode
179			return;
180		}
181
182		match self.code_type.as_ref() {
183			"pre" => {
184				// code block should have its own paragraph
185				if start {
186					printer.insert_newline();
187					printer.append_str(&format!("\n```{}\n", self.lang));
188				}
189				// printer.append_str(&format!("\n```{}\n", self.lang));
190				if !start {
191					printer.append_str("\n```\n");
192					printer.insert_newline();
193				}
194			},
195			"code" | "samp" => printer.append_str("`"),
196			_ => {},
197		}
198	}
199}
200
201impl TagHandler for CodeHandler {
202	fn handle(&mut self, tag: &Handle, printer: &mut StructuredPrinter) {
203		self.code_type = match tag.data {
204			NodeData::Element { ref name, .. } => name.local.to_string(),
205			_ => String::new(),
206		};
207
208		self.do_handle(printer, true);
209	}
210	fn after_handle(&mut self, printer: &mut StructuredPrinter) { self.do_handle(printer, false); }
211}