Skip to content

Commit 90d8064

Browse files
committed
Generate valid XML from doc comments
1 parent 7294268 commit 90d8064

File tree

5 files changed

+112
-28
lines changed

5 files changed

+112
-28
lines changed

godot-core/src/docs.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use std::collections::HashMap;
1818
/// x: f32,
1919
/// }
2020
/// ```
21+
/// All fields are XML parts, escaped where necessary.
2122
#[derive(Clone, Copy, Debug, Default)]
2223
pub struct StructDocs {
2324
pub base: &'static str,
@@ -41,6 +42,7 @@ pub struct StructDocs {
4142
///
4243
/// }
4344
/// ```
45+
/// All fields are XML parts, escaped where necessary.
4446
#[derive(Clone, Copy, Debug, Default)]
4547
pub struct InherentImplDocs {
4648
pub methods: &'static str,
@@ -114,8 +116,7 @@ pub fn gather_xml_docs() -> impl Iterator<Item = String> {
114116
.map(|(x, _)| x)
115117
.unwrap_or_default();
116118

117-
format!(r#"
118-
<?xml version="1.0" encoding="UTF-8"?>
119+
format!(r#"<?xml version="1.0" encoding="UTF-8"?>
119120
<class name="{class}" inherits="{base}" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
120121
<brief_description>{brief}</brief_description>
121122
<description>{description}</description>

godot-macros/src/docs.rs

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,17 @@ pub fn make_definition_docs(
1717
members: &[Field],
1818
) -> TokenStream {
1919
(|| {
20-
let desc = make_docs_from_attributes(description)?;
20+
let base_escaped = xml_escape(base);
21+
let desc_escaped = xml_escape(make_docs_from_attributes(description)?);
2122
let members = members
2223
.into_iter()
2324
.filter(|x| x.var.is_some() | x.export.is_some())
2425
.filter_map(member)
2526
.collect::<String>();
2627
Some(quote! {
2728
docs: ::godot::docs::StructDocs {
28-
base: #base,
29-
description: #desc,
29+
base: #base_escaped,
30+
description: #desc_escaped,
3031
members: #members,
3132
}.into()
3233
})
@@ -122,6 +123,28 @@ fn siphon_docs_from_attributes(doc: &[Attribute]) -> impl Iterator<Item = String
122123
})
123124
}
124125

126+
fn xml_escape(value: String) -> String {
127+
// Most strings have no special characters, so this check helps avoid unnecessary string copying
128+
if !value.contains(&['&', '<', '>', '"', '\'']) {
129+
return value;
130+
}
131+
132+
let mut result = String::with_capacity(value.len());
133+
134+
for c in value.chars() {
135+
match c {
136+
'&' => result.push_str("&amp;"),
137+
'<' => result.push_str("&lt;"),
138+
'>' => result.push_str("&gt;"),
139+
'"' => result.push_str("&quot;"),
140+
'\'' => result.push_str("&#39;"),
141+
c => result.push(c),
142+
}
143+
}
144+
145+
result
146+
}
147+
125148
/// Calls [`siphon_docs_from_attributes`] and converts the result to BBCode
126149
/// for Godot's consumption.
127150
fn make_docs_from_attributes(doc: &[Attribute]) -> Option<String> {
@@ -146,7 +169,9 @@ fn make_signal_docs(signal: &SignalDefinition) -> Option<String> {
146169
{desc}
147170
</description>
148171
</signal>
149-
"#
172+
"#,
173+
name = xml_escape(name.to_string()),
174+
desc = xml_escape(desc),
150175
))
151176
}
152177

@@ -159,7 +184,10 @@ fn make_constant_docs(constant: &Constant) -> Option<String> {
159184
.map(|x| x.to_token_stream().to_string())
160185
.unwrap_or("null".into());
161186
Some(format!(
162-
r#"<constant name="{name}" value="{value}">{docs}</constant>"#
187+
r#"<constant name="{name}" value="{value}">{docs}</constant>"#,
188+
name = xml_escape(name),
189+
value = xml_escape(value),
190+
docs = xml_escape(docs),
163191
))
164192
}
165193

@@ -169,7 +197,11 @@ pub fn member(member: &Field) -> Option<String> {
169197
let ty = member.ty.to_token_stream().to_string();
170198
let default = member.default_val.to_token_stream().to_string();
171199
Some(format!(
172-
r#"<member name="{name}" type="{ty}" default="{default}">{docs}</member>"#
200+
r#"<member name="{name}" type="{ty}" default="{default}">{docs}</member>"#,
201+
name = xml_escape(name.to_string()),
202+
ty = xml_escape(ty),
203+
default = xml_escape(default),
204+
docs = xml_escape(docs),
173205
))
174206
}
175207

@@ -178,7 +210,8 @@ fn params<'a, 'b>(params: impl Iterator<Item = (&'a Ident, &'b TypeExpr)>) -> St
178210
for (index, (name, ty)) in params.enumerate() {
179211
output.push_str(&format!(
180212
r#"<param index="{index}" name="{name}" type="{ty}" />"#,
181-
ty = ty.to_token_stream()
213+
name = xml_escape(name.to_string()),
214+
ty = xml_escape(ty.to_token_stream().to_string()),
182215
));
183216
}
184217
output
@@ -204,7 +237,10 @@ pub fn make_virtual_method_docs(method: Function) -> Option<String> {
204237
{desc}
205238
</description>
206239
</method>
207-
"#
240+
"#,
241+
name = xml_escape(name),
242+
ret = xml_escape(ret),
243+
desc = xml_escape(desc),
208244
))
209245
}
210246

@@ -214,7 +250,7 @@ pub fn make_method_docs(method: &FuncDefinition) -> Option<String> {
214250
.rename
215251
.clone()
216252
.unwrap_or_else(|| method.signature_info.method_name.to_string());
217-
let ret = method.signature_info.ret_type.to_token_stream();
253+
let ret = method.signature_info.ret_type.to_token_stream().to_string();
218254
let params = params(
219255
method
220256
.signature_info
@@ -231,6 +267,9 @@ pub fn make_method_docs(method: &FuncDefinition) -> Option<String> {
231267
{desc}
232268
</description>
233269
</method>
234-
"#
270+
"#,
271+
name = xml_escape(name),
272+
ret = xml_escape(ret),
273+
desc = xml_escape(desc),
235274
))
236275
}

godot-macros/src/docs/markdown_converter.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ fn walk_node(node: &Node, definitions: &HashMap<&str, &str>) -> Option<String> {
6262
List(_) | BlockQuote(_) | FootnoteReference(_) | FootnoteDefinition(_) | Table(_) => {
6363
"".into()
6464
}
65+
Html(html) => html.value.clone(),
6566
_ => walk_nodes(&node.children()?, definitions, ""),
6667
};
6768
Some(bbcode)

godot/tests/docs.rs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
use godot::prelude::*;
99

10-
/// *documented* ~ **documented** ~ [AABB] [pr](https://github.com/godot-rust/gdext/pull/748)
10+
/// *documented* ~ **documented** ~ [AABB] < [pr](https://github.com/godot-rust/gdext/pull/748)
1111
///
1212
/// a few tests:
1313
///
@@ -72,6 +72,22 @@ use godot::prelude::*;
7272
/// static main: u64 = 0x31c0678b10;
7373
/// ```
7474
///
75+
/// Some HTML to make sure it's properly escaped:
76+
///
77+
/// <br/> <- this is inline HTML
78+
///
79+
/// &lt;br/&gt; <- not considered HTML (manually escaped)
80+
///
81+
/// `inline<br/>code`
82+
///
83+
/// ```html
84+
/// <div>
85+
/// code&nbsp;block
86+
/// </div>
87+
/// ```
88+
///
89+
/// [Google: 2 + 2 < 5](https://www.google.com/search?q=2+%2B+2+<+5)
90+
///
7591
/// connect
7692
/// these
7793
#[derive(GodotClass)]
@@ -83,6 +99,9 @@ pub struct FairlyDocumented {
8399
/// is it documented?
84100
#[var]
85101
item_2: i64,
102+
#[var]
103+
/// this docstring has < a special character
104+
item_xml: GString,
86105
/// this isnt documented
87106
_other_item: (),
88107
/// nor this
@@ -97,6 +116,7 @@ impl INode for FairlyDocumented {
97116
base,
98117
item: 883.0,
99118
item_2: 25,
119+
item_xml: "".into(),
100120
_other_item: {},
101121
}
102122
}
@@ -111,6 +131,10 @@ impl FairlyDocumented {
111131
#[constant]
112132
const PURPOSE: i64 = 42;
113133

134+
/// this docstring has < a special character
135+
#[constant]
136+
const XML: i64 = 1;
137+
114138
#[func]
115139
fn totally_undocumented_function(&self) -> i64 {
116140
5
@@ -122,6 +146,12 @@ impl FairlyDocumented {
122146
self.item
123147
}
124148

149+
/// Function with lots of special characters (`Gd<Node>`)
150+
#[func]
151+
fn process_node(&self, node: Gd<Node>) -> Gd<Node> {
152+
node
153+
}
154+
125155
#[func(gd_self, virtual)]
126156
fn virtual_undocumented(_s: Gd<Self>) {
127157
panic!("no implementation")
@@ -130,8 +160,10 @@ impl FairlyDocumented {
130160
/// some virtual function that should be overridden by a user
131161
///
132162
/// some multiline doc
163+
///
164+
/// The `Gd<Node>` param should be properly escaped
133165
#[func(gd_self, virtual)]
134-
fn virtual_documented(_s: Gd<Self>) {
166+
fn virtual_documented(_s: Gd<Self>, _node: Gd<Node>) {
135167
panic!("please provide user implementation")
136168
}
137169

@@ -149,8 +181,10 @@ impl FairlyDocumented {
149181
/// some user signal
150182
///
151183
/// some multiline doc
184+
///
185+
/// The `Gd<Node>` param should be properly escaped
152186
#[signal]
153-
fn documented_signal(p: Vector3, w: f64);
187+
fn documented_signal(p: Vector3, w: f64, node: Gd<Node>);
154188
}
155189

156190
#[test]

godot/tests/test_data/docs.xml

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
21
<?xml version="1.0" encoding="UTF-8"?>
32
<class name="FairlyDocumented" inherits="Node" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
4-
<brief_description>[i]documented[/i] ~ [b]documented[/b] ~ [AABB] [url=https://github.com/godot-rust/gdext/pull/748]pr[/url]</brief_description>
5-
<description>[i]documented[/i] ~ [b]documented[/b] ~ [AABB] [url=https://github.com/godot-rust/gdext/pull/748]pr[/url][br][br]a few tests:[br][br]headings:[br][br]Some heading[br][br]lists:[br][br][br][br][br][br]links with back-references:[br][br]Blah blah [br][br][br][br]footnotes:[br][br]We cannot florbinate the glorb[br][br][br][br]task lists:[br][br]We must ensure that we've completed[br][br][br][br]tables:[br][br][br][br]images:[br][br][img]http://url/a.png[/img][br][br]blockquotes:[br][br][br][br]ordered list:[br][br][br][br]Something here < this is technically header syntax[br][br]And here[br][br]smart punctuation[br][br]codeblocks:[br][br][codeblock]#![no_main]
6-
#[link_section=\".text\"]
3+
<brief_description>[i]documented[/i] ~ [b]documented[/b] ~ [AABB] &lt; [url=https://github.com/godot-rust/gdext/pull/748]pr[/url]</brief_description>
4+
<description>[i]documented[/i] ~ [b]documented[/b] ~ [AABB] &lt; [url=https://github.com/godot-rust/gdext/pull/748]pr[/url][br][br]a few tests:[br][br]headings:[br][br]Some heading[br][br]lists:[br][br][br][br][br][br]links with back-references:[br][br]Blah blah [br][br][br][br]footnotes:[br][br]We cannot florbinate the glorb[br][br][br][br]task lists:[br][br]We must ensure that we&#39;ve completed[br][br][br][br]tables:[br][br][br][br]images:[br][br][img]http://url/a.png[/img][br][br]blockquotes:[br][br][br][br]ordered list:[br][br][br][br]Something here &lt; this is technically header syntax[br][br]And here[br][br]smart punctuation[br][br]codeblocks:[br][br][codeblock]#![no_main]
5+
#[link_section=\&quot;.text\&quot;]
76
#[no_mangle]
8-
static main: u64 = 0x31c0678b10;[/codeblock][br][br]connect
7+
static main: u64 = 0x31c0678b10;[/codeblock][br][br]Some HTML to make sure it&#39;s properly escaped:[br][br]&lt;br/&gt; &lt;- this is inline HTML[br][br]&lt;br/&gt; &lt;- not considered HTML (manually escaped)[br][br][code]inline&lt;br/&gt;code[/code][br][br][codeblock]&lt;div&gt;
8+
code&amp;nbsp;block
9+
&lt;/div&gt;[/codeblock][br][br][url=https://www.google.com/search?q=2+%2B+2+&lt;+5]Google: 2 + 2 &lt; 5[/url][br][br]connect
910
these</description>
1011
<methods>
1112
<method name="ye">
@@ -16,16 +17,24 @@ these</description>
1617
</description>
1718
</method>
1819

20+
<method name="process_node">
21+
<return type="Gd &lt; Node &gt;" />
22+
<param index="0" name="node" type="Gd &lt; Node &gt;" />
23+
<description>
24+
Function with lots of special characters ([code]Gd&lt;Node&gt;[/code])
25+
</description>
26+
</method>
27+
1928
<method name="virtual_documented">
2029
<return type="()" />
21-
30+
<param index="0" name="node" type="Gd &lt; Node &gt;" />
2231
<description>
23-
some virtual function that should be overridden by a user[br][br]some multiline doc
32+
some virtual function that should be overridden by a user[br][br]some multiline doc[br][br]The [code]Gd&lt;Node&gt;[/code] param should be properly escaped
2433
</description>
2534
</method>
2635

2736
<method name="ne">
28-
<return type="Gd < FairlyDocumented >" />
37+
<return type="Gd &lt; FairlyDocumented &gt;" />
2938
<param index="0" name="x" type="f32" />
3039
<description>
3140
wow[br][br]some multiline doc
@@ -34,20 +43,20 @@ these</description>
3443

3544
<method name="_init">
3645
<return type="Self" />
37-
<param index="0" name="base" type="Base < Node >" />
46+
<param index="0" name="base" type="Base &lt; Node &gt;" />
3847
<description>
3948
initialize this
4049
</description>
4150
</method>
4251
</methods>
43-
<constants><constant name="RANDOM" value="4">Documentation.</constant></constants>
52+
<constants><constant name="RANDOM" value="4">Documentation.</constant><constant name="XML" value="1">this docstring has &lt; a special character</constant></constants>
4453
<signals>
4554
<signal name="documented_signal">
46-
<param index="0" name="p" type="Vector3" /><param index="1" name="w" type="f64" />
55+
<param index="0" name="p" type="Vector3" /><param index="1" name="w" type="f64" /><param index="2" name="node" type="Gd &lt; Node &gt;" />
4756
<description>
48-
some user signal[br][br]some multiline doc
57+
some user signal[br][br]some multiline doc[br][br]The [code]Gd&lt;Node&gt;[/code] param should be properly escaped
4958
</description>
5059
</signal>
5160
</signals>
52-
<members><member name="item" type="f32" default="">this is very documented</member><member name="item_2" type="i64" default="">is it documented?</member></members>
61+
<members><member name="item" type="f32" default="">this is very documented</member><member name="item_2" type="i64" default="">is it documented?</member><member name="item_xml" type="GString" default="">this docstring has &lt; a special character</member></members>
5362
</class>

0 commit comments

Comments
 (0)