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 markup5ever_rcdom::NodeData;
13use markup5ever_rcdom::RcDom;
14
15use html5ever::parse_document;
16use html5ever::tendril::TendrilSink;
17use html5ever::tree_builder::TreeSink;
18use markup5ever_rcdom::Handle;
19use markup5ever_rcdom::SerializableHandle;
20use utils::toolchain::sdk::Sdk;
21
22use crate::Result;
23use super::DocsMap;
24
25
26pub fn parse(sdk: &Sdk) -> Result<DocsMap> {
27	let path = sdk.path().join("Inside Playdate with C.html");
28	parse_file(&path).map_err(Into::into)
29}
30
31
32pub fn parse_file(path: &std::path::Path) -> Result<DocsMap, IoError> {
33	if !path.try_exists()? {
34		return Err(IoError::new(ErrorKind::NotFound, path.display().to_string()));
35	}
36
37	let mut results = DocsMap::new();
38	let dom = parse_document(RcDom::default(), Default::default()).from_utf8()
39	                                                              .from_file(path)?
40	                                                              .finish();
41	if !dom.errors.is_empty() {
42		eprintln!("errors: {:#?}", dom.errors);
43	}
44
45	walk(&dom.document, &mut results);
46
47	#[cfg(feature = "log")]
48	for (k, _) in &results {
49		println!("Doc found for {k}");
50	}
51
52	Ok(results)
53}
54
55
56// TODO: optimize, use Cow instead of String.
57fn walk(handle: &Handle, results: &mut DocsMap) {
58	let node = handle;
59	let mut found = None;
60	if let NodeData::Element { ref name, ref attrs, .. } = node.data {
61		found = if name.local == *"div" {
62			let attrs = attrs.borrow();
63			let attr = attrs.iter()
64			                .find(|attr| attr.name.local == *"id" && attr.value.starts_with("f-"));
65			attr.map(|attr| {
66				    attr.value
67				        .strip_prefix("f-")
68				        .expect("prefix 'f-' must be there")
69				        .to_string()
70			    })
71		} else {
72			None
73		};
74
75		if let Some(_key) = found.as_ref() {
76			// Changing: this-div . div_class="title" is a cpp-code fn-path+definition
77			//                  to `<code data-lang="c">`
78			// TODO: fix links like `<a href="#f-sound.source">SoundSource</a>`
79			let mut children = node.children.borrow_mut();
80			let title = children.iter_mut().find(|child| {
81				                               match &child.data {
82					                               NodeData::Element { name, attrs, .. } => {
83					                                  name.local.eq("div") &&
84					                                  attrs.borrow().iter().any(|attr| {
85						                                                       attr.name.local.eq("class") &&
86						                                                       attr.value.contains("title")
87					                                                       })
88				                                  },
89				                                  _ => false,
90				                               }
91			                               });
92			if let Some(title) = title {
93				let mut data = {
94					match &title.data {
95						NodeData::Element { name,
96						                    attrs,
97						                    template_contents,
98						                    mathml_annotation_xml_integration_point, } => {
99							let mut code = name.clone();
100							code.borrow_mut().local = html5ever::LocalName::from("code");
101							NodeData::Element { name: code,
102							                    attrs: attrs.clone(),
103							                    template_contents: template_contents.clone(),
104							                    mathml_annotation_xml_integration_point:
105								                    *mathml_annotation_xml_integration_point }.into()
106						},
107						_ => None,
108					}
109				};
110
111				if let Some(data) = data.take() {
112					unsafe {
113						std::rc::Rc::get_mut_unchecked(title).data = data;
114					}
115				}
116			}
117		}
118	}
119
120	for child in
121		node.children
122		    .borrow()
123		    .iter()
124		    .filter(|child| matches!(child.data, NodeData::Text { .. } | NodeData::Element { .. }))
125	{
126		walk(child, results);
127	}
128
129	if let Some(key) = found {
130		let document: SerializableHandle = node.clone().into();
131		let mut render = Vec::new();
132		html5ever::serialize(&mut render, &document, Default::default()).expect("serialization failed");
133		let html = std::str::from_utf8(&render).unwrap();
134		let mut node = html2md::parser::safe_parse_html(html.to_owned()).expect("parsing failed");
135		if let Some(node) = node.children.first_mut() {
136			if node.tag_name == Some(html2md::structs::NodeType::Code) &&
137			   node.attributes
138			       .as_ref()
139			       .filter(|a| a.get_class().map(String::as_str) == Some("title"))
140			       .is_some()
141			{
142				node.children.clear();
143			}
144		}
145
146		let mut md = html2md::to_md::to_md(node);
147		md = md.strip_prefix("```\n```\n")
148		       .map(ToString::to_string)
149		       .unwrap_or(md);
150		if !md.trim().is_empty() {
151			results.insert(key, md);
152		}
153	}
154}