Skip to content

Commit b6dcf6e

Browse files
bors[bot]Bromeon
andauthored
Merge #779
779: Parse Godot documentation's BBCode and translate it to RustDoc r=Bromeon a=Bromeon Closes #590. Parse Godot documentation's BBCode and translate it to RustDoc, so that our API doc looks nicer. In particular, many intra-doc links are now recognized. As a result, it's now much easier to navigate through the GDNative bindings' doc. The parser is not perfect, there are also some missing parts, but it should already be a big improvement over weird BBCode syntax. bors try Co-authored-by: Jan Haller <[email protected]>
2 parents a336a25 + a2a3c51 commit b6dcf6e

File tree

3 files changed

+147
-12
lines changed

3 files changed

+147
-12
lines changed

bindings_generator/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ quote = "1.0.9"
2121
syn = { version = "1.0.74", features = ["full", "extra-traits", "visit"] }
2222
miniserde = "0.1.14"
2323
unindent = "0.1.7"
24+
regex = "1.5.4"
25+
memchr = "2.4" # to satisfy regex needing memchr >= 2.4

bindings_generator/src/api.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,7 @@ pub enum Ty {
357357
}
358358

359359
impl Ty {
360+
// Note: there is some code duplication with GodotXmlDocs::translate_type() in class_docs.rs
360361
pub fn from_src(src: &str) -> Self {
361362
match src {
362363
"void" => Ty::Void,

bindings_generator/src/class_docs.rs

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use std::{collections::HashMap, fs};
22

3+
use regex::{Captures, Regex};
34
use roxmltree::Node;
45

5-
#[derive(Debug)]
66
pub struct GodotXmlDocs {
77
class_fn_desc: HashMap<(String, String), String>,
8+
regexes: Regexes,
89
}
910

1011
impl GodotXmlDocs {
@@ -16,6 +17,7 @@ impl GodotXmlDocs {
1617

1718
let mut docs = GodotXmlDocs {
1819
class_fn_desc: HashMap::default(),
20+
regexes: Regexes::new(),
1921
};
2022

2123
for entry in entries {
@@ -126,27 +128,157 @@ impl GodotXmlDocs {
126128

127129
self.class_fn_desc.insert(
128130
(class.into(), method.into()),
129-
Self::reformat_as_rustdoc(doc),
131+
Self::reformat_as_rustdoc(&self.regexes, doc),
130132
);
131133
}
132134

135+
// For types that godot-rust names differently than Godot
136+
fn translate_type(godot_type: &str) -> &str {
137+
// Note: there is some code duplication with Ty::from_src() in api.rs
138+
match godot_type {
139+
"String" => "GodotString",
140+
"Error" => "GodotError",
141+
"RID" => "Rid",
142+
"AABB" => "Aabb",
143+
"Array" => "VariantArray",
144+
"PoolByteArray" => "ByteArray",
145+
"PoolStringArray" => "StringArray",
146+
"PoolVector2Array" => "Vector2Array",
147+
"PoolVector3Array" => "Vector3Array",
148+
"PoolColorArray" => "ColorArray",
149+
"PoolIntArray" => "Int32Array",
150+
"PoolRealArray" => "Float32Array",
151+
"G6DOFJointAxisParam" => "G6dofJointAxisParam",
152+
"G6DOFJointAxisFlag" => "G6dofJointAxisFlag",
153+
_ => godot_type,
154+
}
155+
}
156+
133157
/// Takes the Godot documentation markup and transforms it to Rustdoc.
134-
/// Very basic approach with limitations, but already helps readability quite a bit.
135-
fn reformat_as_rustdoc(godot_doc: String) -> String {
136-
let gdscript_note = if godot_doc.contains("[codeblock]") {
137-
"_Sample code is GDScript unless otherwise noted._\n\n"
158+
/// Replaces BBCode syntax with Rustdoc/Markdown equivalents and implements working intra-doc links.
159+
fn reformat_as_rustdoc(re: &Regexes, godot_doc: String) -> String {
160+
// Note: there are still a few unsupported cases, such as:
161+
// * OK and ERR_CANT_CREATE (corresponding Result.Ok() and GodotError.ERR_CANT_CREATE)
162+
// * "indexed properties" which are not also exposed as getters, e.g. `gravity_point` in
163+
// https://docs.godotengine.org/en/stable/classes/class_area2d.html#properties.
164+
// This needs to be implemented first: https://github.com/godot-rust/godot-rust/issues/689
165+
166+
// Info for GDScript blocks
167+
let godot_doc = if godot_doc.contains("[codeblock]") {
168+
format!(
169+
"_Sample code is GDScript unless otherwise noted._\n\n{}",
170+
godot_doc
171+
)
138172
} else {
139-
""
173+
godot_doc
140174
};
141175

142-
let translated = godot_doc
143-
.replace("[code]", "`")
144-
.replace("[/code]", "`")
176+
// Before any regex replacement, do verbatim replacements
177+
// Note: maybe some can be expressed as regex, but if text-replace does the job reliably enough, it's even faster
178+
let godot_doc = godot_doc
145179
.replace("[codeblock]", "```gdscript")
146180
.replace("[/codeblock]", "```")
181+
.replace("[code]", "`")
182+
.replace("[/code]", "`")
147183
.replace("[b]", "**")
148-
.replace("[/b]", "**");
184+
.replace("[/b]", "**")
185+
.replace("[i]", "_")
186+
.replace("[/i]", "_");
187+
188+
// URLs
189+
let godot_doc = re.url.replace_all(&godot_doc, |c: &Captures| {
190+
let url = &c[1];
191+
let text = &c[2];
192+
193+
if text.is_empty() {
194+
format!("<{url}>", url = url)
195+
} else {
196+
format!("[{text}]({url})", text = text, url = url)
197+
}
198+
});
199+
200+
// [Type::member] style
201+
let godot_doc = re.class_member.replace_all(&godot_doc, |c: &Captures| {
202+
let godot_ty = &c[2];
203+
let rust_ty = Self::translate_type(godot_ty);
204+
205+
format!(
206+
"[`{godot_ty}.{member}`][{rust_ty}::{member}]",
207+
godot_ty = godot_ty,
208+
rust_ty = rust_ty,
209+
member = &c[3]
210+
)
211+
});
212+
213+
// [member] style
214+
let godot_doc = re.self_member.replace_all(&godot_doc, |c: &Captures| {
215+
format!("[`{member}`][Self::{member}]", member = &c[2])
216+
});
149217

150-
format!("{}{}", gdscript_note, translated)
218+
// `member` style (no link)
219+
let godot_doc = re.no_link.replace_all(&godot_doc, |c: &Captures| {
220+
format!("`{member}`", member = &c[1])
221+
});
222+
223+
// [Type] style
224+
let godot_doc = re.class.replace_all(&godot_doc, |c: &Captures| {
225+
let godot_ty = &c[2];
226+
let rust_ty = Self::translate_type(godot_ty);
227+
228+
format!(
229+
"[`{godot_ty}`][{rust_ty}]",
230+
godot_ty = godot_ty,
231+
rust_ty = rust_ty
232+
)
233+
});
234+
235+
godot_doc.to_string()
236+
}
237+
}
238+
239+
// Holds several compiled regexes to reuse across classes
240+
// could also use 'lazy_regex' crate, but standard 'regex' has better IDE support and works well enough
241+
struct Regexes {
242+
url: Regex,
243+
no_link: Regex,
244+
class: Regex,
245+
self_member: Regex,
246+
class_member: Regex,
247+
}
248+
249+
impl Regexes {
250+
fn new() -> Self {
251+
Self {
252+
// Covers:
253+
// * [url=U]text[/url]
254+
// * [url=U][/url]
255+
url: Regex::new("\\[url=(.+?)](.*?)\\[/url]").unwrap(),
256+
257+
// Covers:
258+
// * [code]C[/code]
259+
// * [signal C]
260+
// Must run before others, as [code] will itself match the link syntax
261+
no_link: Regex::new("\\[signal ([A-Za-z0-9_]+?)]").unwrap(),
262+
263+
// Covers:
264+
// * [C]
265+
// * [enum C]
266+
class: Regex::new("\\[(enum )?([A-Za-z0-9_]+?)]").unwrap(),
267+
268+
// Covers:
269+
// * [member M]
270+
// * [method M]
271+
// * [constant M]
272+
self_member: Regex::new("\\[(member|method|constant) ([A-Za-z0-9_]+?)]").unwrap(),
273+
274+
// Covers:
275+
// * [member C.M]
276+
// * [method C.M]
277+
// * [constant C.M]
278+
class_member: Regex::new(
279+
"\\[(member|method|constant) ([A-Za-z0-9_]+?)\\.([A-Za-z0-9_]+?)]",
280+
)
281+
.unwrap(),
282+
}
151283
}
152284
}

0 commit comments

Comments
 (0)