From b586b801640882b6783cc6a7f79d08403c346a52 Mon Sep 17 00:00:00 2001 From: bendn Date: Tue, 18 Jun 2024 10:19:14 +0700 Subject: [PATCH] docs --- godot-core/Cargo.toml | 1 + godot-core/src/docs.rs | 114 +++++++++ godot-core/src/init/mod.rs | 13 +- godot-core/src/lib.rs | 13 + godot-core/src/registry/class.rs | 6 + godot-core/src/registry/plugin.rs | 14 +- godot-macros/Cargo.toml | 3 + godot-macros/src/class/data_models/field.rs | 4 + .../src/class/data_models/inherent_impl.rs | 6 + .../class/data_models/interface_trait_impl.rs | 6 +- godot-macros/src/class/derive_godot_class.rs | 20 +- godot-macros/src/docs.rs | 226 ++++++++++++++++++ godot-macros/src/docs/markdown_converter.rs | 77 ++++++ godot-macros/src/lib.rs | 33 ++- godot/Cargo.toml | 2 + godot/src/lib.rs | 10 + godot/tests/docs.rs | 131 ++++++++++ godot/tests/docs.xml | 38 +++ 18 files changed, 704 insertions(+), 13 deletions(-) create mode 100644 godot-core/src/docs.rs create mode 100644 godot-macros/src/docs.rs create mode 100644 godot-macros/src/docs/markdown_converter.rs create mode 100644 godot/tests/docs.rs create mode 100644 godot/tests/docs.xml diff --git a/godot-core/Cargo.toml b/godot-core/Cargo.toml index d50981cfe..b0cf4cb90 100644 --- a/godot-core/Cargo.toml +++ b/godot-core/Cargo.toml @@ -12,6 +12,7 @@ homepage = "https://godot-rust.github.io" [features] default = [] +docs = [] codegen-rustfmt = ["godot-ffi/codegen-rustfmt", "godot-codegen/codegen-rustfmt"] codegen-full = ["godot-codegen/codegen-full"] codegen-lazy-fptrs = [ diff --git a/godot-core/src/docs.rs b/godot-core/src/docs.rs new file mode 100644 index 000000000..4f6da61e9 --- /dev/null +++ b/godot-core/src/docs.rs @@ -0,0 +1,114 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use crate::registry::plugin::PluginItem; +use std::collections::HashMap; + +/// Created for documentation on +/// ```ignore +/// #[derive(GodotClass)] +/// /// Documented +/// struct Struct { +/// /// documented +/// x: f32, +/// } +/// ``` +#[derive(Clone, Copy, Debug, Default)] +pub struct StructDocs { + pub base: &'static str, + pub description: &'static str, + pub members: &'static str, +} + +/// Created for documentation on +/// ```ignore +/// #[godot_api] +/// impl Struct { +/// #[func] +/// /// This function panics! +/// fn panic() -> f32 { panic!() } +/// } +/// ``` +#[derive(Clone, Copy, Debug, Default)] +pub struct InherentImplDocs { + pub methods: &'static str, + pub signals: &'static str, + pub constants: &'static str, +} + +#[derive(Default)] +struct DocPieces { + definition: StructDocs, + inherent: InherentImplDocs, + virtual_methods: &'static str, +} + +#[doc(hidden)] +/// This function scours the registered plugins to find their documentation pieces, +/// and strings them together. +/// +/// It returns an iterator over XML documents. +pub fn gather_xml_docs() -> impl Iterator { + let mut map = HashMap::<&'static str, DocPieces>::new(); + crate::private::iterate_plugins(|x| match x.item { + PluginItem::InherentImpl { + docs: Some(docs), .. + } => map.entry(x.class_name.as_str()).or_default().inherent = docs, + PluginItem::ITraitImpl { + virtual_method_docs, + .. + } => { + map.entry(x.class_name.as_str()) + .or_default() + .virtual_methods = virtual_method_docs + } + PluginItem::Struct { + docs: Some(docs), .. + } => map.entry(x.class_name.as_str()).or_default().definition = docs, + _ => (), + }); + map.into_iter().map(|(class, pieces)| { + let StructDocs { + base, + description, + members, + } = pieces.definition; + + let InherentImplDocs { + methods, + signals, + constants, + } = pieces.inherent; + + let virtual_methods = pieces.virtual_methods; + let brief = description.split_once("[br]").map(|(x, _)| x).unwrap_or_default(); +format!(r#" + + +{brief} +{description} +{methods}{virtual_methods} +{constants} +{signals} +{members} +"#) + }, + ) +} + +/// # Safety +/// +/// The Godot binding must have been initialized before calling this function. +/// +/// If "experimental-threads" is not enabled, then this must be called from the same thread that the bindings were initialized from. +pub unsafe fn register() { + for xml in gather_xml_docs() { + crate::sys::interface_fn!(editor_help_load_xml_from_utf8_chars_and_len)( + xml.as_ptr() as *const std::ffi::c_char, + xml.len() as i64, + ); + } +} diff --git a/godot-core/src/init/mod.rs b/godot-core/src/init/mod.rs index ae06a6556..fcce15b09 100644 --- a/godot-core/src/init/mod.rs +++ b/godot-core/src/init/mod.rs @@ -137,9 +137,16 @@ unsafe fn gdext_on_level_init(level: InitLevel) { // SAFETY: we are in the main thread, initialize has been called, has never been called with this level before. unsafe { sys::load_class_method_table(level) }; - if level == InitLevel::Scene { - // SAFETY: On the main thread, api initialized, `Scene` was initialized above. - unsafe { ensure_godot_features_compatible() }; + match level { + InitLevel::Scene => { + // SAFETY: On the main thread, api initialized, `Scene` was initialized above. + unsafe { ensure_godot_features_compatible() }; + } + InitLevel::Editor => { + #[cfg(all(since_api = "4.3", feature = "docs"))] + crate::docs::register(); + } + _ => (), } crate::registry::class::auto_register_classes(level); diff --git a/godot-core/src/lib.rs b/godot-core/src/lib.rs index 63816be4c..34d64443e 100644 --- a/godot-core/src/lib.rs +++ b/godot-core/src/lib.rs @@ -15,6 +15,13 @@ pub mod builder; pub mod builtin; pub mod classes; +#[cfg(all(since_api = "4.3", feature = "docs"))] +pub mod docs; +#[doc(hidden)] +pub mod possibly_docs { + #[cfg(all(since_api = "4.3", feature = "docs"))] + pub use crate::docs::*; +} pub mod global; pub mod init; pub mod meta; @@ -70,3 +77,9 @@ pub mod log { godot_error, godot_print, godot_print_rich, godot_script_error, godot_warn, }; } + +// ---- +// Validation + +#[cfg(all(feature = "docs", before_api = "4.3"))] +compile_error!("Documentation generation requires 4.3."); diff --git a/godot-core/src/registry/class.rs b/godot-core/src/registry/class.rs index f21e77681..1fe8e65a5 100644 --- a/godot-core/src/registry/class.rs +++ b/godot-core/src/registry/class.rs @@ -232,6 +232,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { is_editor_plugin, is_hidden, is_instantiable, + #[cfg(all(since_api = "4.3", feature = "docs"))] + docs: _, } => { c.parent_class_name = Some(base_class_name); c.default_virtual_fn = default_get_virtual_fn; @@ -282,6 +284,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { PluginItem::InherentImpl { register_methods_constants_fn, + #[cfg(all(since_api = "4.3", feature = "docs"))] + docs: _, } => { c.register_methods_constants_fn = Some(register_methods_constants_fn); } @@ -299,6 +303,8 @@ fn fill_class_info(item: PluginItem, c: &mut ClassRegistrationInfo) { user_free_property_list_fn, user_property_can_revert_fn, user_property_get_revert_fn, + #[cfg(all(since_api = "4.3", feature = "docs"))] + virtual_method_docs: _, } => { c.user_register_fn = user_register_fn; diff --git a/godot-core/src/registry/plugin.rs b/godot-core/src/registry/plugin.rs index b0767c160..d5220cf0e 100644 --- a/godot-core/src/registry/plugin.rs +++ b/godot-core/src/registry/plugin.rs @@ -5,12 +5,13 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::any::Any; -use std::fmt; - +#[cfg(all(since_api = "4.3", feature = "docs"))] +use crate::docs::*; use crate::init::InitLevel; use crate::meta::ClassName; use crate::sys; +use std::any::Any; +use std::fmt; // TODO(bromeon): some information coming from the proc-macro API is deferred through PluginItem, while others is directly // translated to code. Consider moving more code to the PluginItem, which allows for more dynamic registration and will @@ -96,6 +97,8 @@ pub enum PluginItem { /// Whether the class has a default constructor. is_instantiable: bool, + #[cfg(all(since_api = "4.3", feature = "docs"))] + docs: Option, }, /// Collected from `#[godot_api] impl MyClass`. @@ -104,10 +107,15 @@ pub enum PluginItem { /// /// Always present since that's the entire point of this `impl` block. register_methods_constants_fn: ErasedRegisterFn, + #[cfg(all(since_api = "4.3", feature = "docs"))] + docs: Option, }, /// Collected from `#[godot_api] impl I... for MyClass`. ITraitImpl { + #[cfg(all(since_api = "4.3", feature = "docs"))] + /// Virtual method documentation. + virtual_method_docs: &'static str, /// Callback to user-defined `register_class` function. user_register_fn: Option, diff --git a/godot-macros/Cargo.toml b/godot-macros/Cargo.toml index d67fa0774..6d1ecbef9 100644 --- a/godot-macros/Cargo.toml +++ b/godot-macros/Cargo.toml @@ -12,6 +12,7 @@ homepage = "https://godot-rust.github.io" [features] api-custom = ["godot-bindings/api-custom"] +docs = ["dep:markdown"] [lib] proc-macro = true @@ -21,6 +22,8 @@ proc-macro = true proc-macro2 = "1.0.63" quote = "1.0.29" +# Enabled by `docs` +markdown = { version = "1.0.0-alpha.17", optional = true } venial = "0.6" [build-dependencies] diff --git a/godot-macros/src/class/data_models/field.rs b/godot-macros/src/class/data_models/field.rs index e3cae325e..160a72a4e 100644 --- a/godot-macros/src/class/data_models/field.rs +++ b/godot-macros/src/class/data_models/field.rs @@ -15,6 +15,8 @@ pub struct Field { pub var: Option, pub export: Option, pub is_onready: bool, + #[cfg(feature = "docs")] + pub attributes: Vec, } impl Field { @@ -26,6 +28,8 @@ impl Field { var: None, export: None, is_onready: false, + #[cfg(feature = "docs")] + attributes: field.attributes.clone(), } } } diff --git a/godot-macros/src/class/data_models/inherent_impl.rs b/godot-macros/src/class/data_models/inherent_impl.rs index 830e1764e..25d4e9d14 100644 --- a/godot-macros/src/class/data_models/inherent_impl.rs +++ b/godot-macros/src/class/data_models/inherent_impl.rs @@ -51,6 +51,11 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult = funcs @@ -80,6 +85,7 @@ pub fn transform_inherent_impl(mut impl_block: venial::Impl) -> ParseResult, }, + #docs }, init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, }); diff --git a/godot-macros/src/class/data_models/interface_trait_impl.rs b/godot-macros/src/class/data_models/interface_trait_impl.rs index 08901e9b7..e6fa5e371 100644 --- a/godot-macros/src/class/data_models/interface_trait_impl.rs +++ b/godot-macros/src/class/data_models/interface_trait_impl.rs @@ -42,7 +42,10 @@ pub fn transform_trait_impl(original_impl: venial::Impl) -> ParseResult ParseResult, + #docs }, init_level: <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL, }); diff --git a/godot-macros/src/class/derive_godot_class.rs b/godot-macros/src/class/derive_godot_class.rs index dbce69da6..de4c6b55e 100644 --- a/godot-macros/src/class/derive_godot_class.rs +++ b/godot-macros/src/class/derive_godot_class.rs @@ -35,6 +35,14 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { let is_editor_plugin = struct_cfg.is_editor_plugin; let is_hidden = struct_cfg.is_hidden; let base_ty = &struct_cfg.base_ty; + #[cfg(all(feature = "docs", since_api = "4.3"))] + let docs = crate::docs::make_definition_docs( + base_ty.to_string(), + &class.attributes, + &fields.all_fields, + ); + #[cfg(not(all(feature = "docs", since_api = "4.3")))] + let docs = quote! {}; let base_class = quote! { ::godot::classes::#base_ty }; let base_class_name_obj = util::class_name_obj(&base_class); let inherits_macro = format_ident!("unsafe_inherits_transitive_{}", base_ty); @@ -75,7 +83,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { match struct_cfg.init_strategy { InitStrategy::Generated => { - godot_init_impl = make_godot_init_impl(class_name, fields); + godot_init_impl = make_godot_init_impl(class_name, &fields); create_fn = quote! { Some(#prv::callbacks::create::<#class_name>) }; if cfg!(since_api = "4.2") { @@ -142,6 +150,7 @@ pub fn derive_godot_class(item: venial::Item) -> ParseResult { is_editor_plugin: #is_editor_plugin, is_hidden: #is_hidden, is_instantiable: #is_instantiable, + #docs }, init_level: { let level = <#class_name as ::godot::obj::GodotClass>::INIT_LEVEL; @@ -193,17 +202,18 @@ struct ClassAttributes { rename: Option, } -fn make_godot_init_impl(class_name: &Ident, fields: Fields) -> TokenStream { - let base_init = if let Some(Field { name, .. }) = fields.base_field { +fn make_godot_init_impl(class_name: &Ident, fields: &Fields) -> TokenStream { + let base_init = if let Some(Field { name, .. }) = &fields.base_field { quote! { #name: base, } } else { TokenStream::new() }; - let rest_init = fields.all_fields.into_iter().map(|field| { - let field_name = field.name; + let rest_init = fields.all_fields.iter().map(|field| { + let field_name = field.name.clone(); let value_expr = field .default + .clone() .unwrap_or_else(|| quote! { ::std::default::Default::default() }); quote! { #field_name: #value_expr, } diff --git a/godot-macros/src/docs.rs b/godot-macros/src/docs.rs new file mode 100644 index 000000000..b62be3129 --- /dev/null +++ b/godot-macros/src/docs.rs @@ -0,0 +1,226 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +mod markdown_converter; + +use crate::class::{ConstDefinition, Field, FuncDefinition, SignalDefinition}; +use proc_macro2::{Ident, TokenStream}; +use quote::{quote, ToTokens}; +use venial::*; + +pub fn make_definition_docs( + base: String, + description: &[Attribute], + members: &[Field], +) -> TokenStream { + (|| { + let desc = make_docs_from_attributes(description)?; + let members = members + .into_iter() + .filter(|x| x.var.is_some() | x.export.is_some()) + .map(member) + .collect::>()?; + Some(quote! { + docs: ::godot::docs::StructDocs { + base: #base, + description: #desc, + members: #members, + }.into() + }) + })() + .unwrap_or(quote! { docs: None }) +} + +pub fn make_inherent_impl_docs( + functions: &[FuncDefinition], + constants: &[ConstDefinition], + signals: &[SignalDefinition], +) -> TokenStream { + fn pieces( + functions: &[FuncDefinition], + signals: &[SignalDefinition], + constants: &[ConstDefinition], + ) -> Option { + let methods = functions + .iter() + .map(make_method_docs) + .collect::>()?; + let signals = signals + .iter() + .map(make_signal_docs) + .collect::>()?; + let constants = constants + .iter() + .map(|ConstDefinition { raw_constant: x }| x) + .map(make_constant_docs) + .collect::>()?; + let field_definition = quote! { + docs: ::godot::docs::InherentImplDocs { + methods: #methods, + signals: #signals, + constants: #constants, + }.into() + }; + Some(field_definition) + } + pieces(functions, signals, constants).unwrap_or_else(|| quote! { docs: None }) +} + +pub fn make_virtual_impl_docs(vmethods: &[ImplMember]) -> TokenStream { + match vmethods + .iter() + .filter_map(|x| match x { + venial::ImplMember::AssocFunction(f) => Some(f.clone()), + _ => None, + }) + .map(make_virtual_method_docs) + .collect::>() + { + Some(vmethods) => quote! { + virtual_method_docs: #vmethods, + }, + None => quote! { + virtual_method_docs: "" + }, + } +} + +/// `///` is expanded to `#[doc = "…"]`. +/// This function goes through and extracts the … +fn siphon_docs_from_attributes(doc: &[Attribute]) -> impl Iterator + '_ { + doc.iter() + // find #[doc] + .filter(|x| x.get_single_path_segment().is_some_and(|x| x == "doc")) + // #[doc = "…"] + .filter_map(|x| match &x.value { + AttributeValue::Equals(_, doc) => Some(doc), + _ => None, + }) + .flat_map(|doc| { + doc.into_iter().map(|x| { + x.to_string() + .trim_start_matches('r') + .trim_start_matches('#') + .trim_start_matches('"') + .trim_end_matches('#') + .trim_end_matches('"') + .to_string() + }) + }) +} + +/// Calls [`siphon_docs_from_attributes`] and converts the result to BBCode +/// for Godot's consumption. +fn make_docs_from_attributes(doc: &[Attribute]) -> Option { + let doc = siphon_docs_from_attributes(doc) + .collect::>() + .join("\n"); + (!doc.is_empty()).then(|| markdown_converter::to_bbcode(&doc)) +} + +fn make_signal_docs(signal: &SignalDefinition) -> Option { + let name = &signal.signature.name; + let params = params(signal.signature.params.iter().filter_map(|(x, _)| match x { + FnParam::Receiver(_) => None, + FnParam::Typed(y) => Some((&y.name, &y.ty)), + })); + let desc = make_docs_from_attributes(&signal.external_attributes)?; + Some(format!( + r#" + + {params} + + {desc} + + +"# + )) +} + +fn make_constant_docs(constant: &Constant) -> Option { + let docs = make_docs_from_attributes(&constant.attributes)?; + let name = constant.name.to_string(); + let value = constant + .initializer + .as_ref() + .map(|x| x.to_token_stream().to_string()) + .unwrap_or("null".into()); + Some(format!( + r#"{docs}"# + )) +} + +pub fn member(member: &Field) -> Option { + let docs = make_docs_from_attributes(&member.attributes)?; + let name = &member.name; + let ty = member.ty.to_token_stream().to_string(); + let default = member.default.to_token_stream().to_string(); + Some(format!( + r#"{docs}"# + )) +} + +fn params<'a, 'b>(params: impl Iterator) -> String { + let mut output = String::new(); + for (index, (name, ty)) in params.enumerate() { + output.push_str(&format!( + r#""#, + ty = ty.to_token_stream() + )); + } + output +} + +pub fn make_virtual_method_docs(method: Function) -> Option { + let desc = make_docs_from_attributes(&method.attributes)?; + let name = method.name.to_string(); + let ret = method + .return_ty + .map(|x| x.to_token_stream().to_string()) + .unwrap_or("void".into()); + let params = params(method.params.iter().filter_map(|(x, _)| match x { + FnParam::Receiver(_) => None, + FnParam::Typed(y) => Some((&y.name, &y.ty)), + })); + Some(format!( + r#" + + + {params} + + {desc} + + +"# + )) +} + +pub fn make_method_docs(method: &FuncDefinition) -> Option { + let desc = make_docs_from_attributes(&method.external_attributes)?; + let name = method + .rename + .clone() + .unwrap_or_else(|| method.signature_info.method_name.to_string()); + let ret = method.signature_info.ret_type.to_token_stream(); + let params = params( + method + .signature_info + .param_idents + .iter() + .zip(&method.signature_info.param_types), + ); + Some(format!( + r#" + + + {params} + + {desc} + + +"# + )) +} diff --git a/godot-macros/src/docs/markdown_converter.rs b/godot-macros/src/docs/markdown_converter.rs new file mode 100644 index 000000000..c9cf77b98 --- /dev/null +++ b/godot-macros/src/docs/markdown_converter.rs @@ -0,0 +1,77 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +//! Converts [Markdown](https://en.wikipedia.org/wiki/Markdown) to [BBCode](https://en.wikipedia.org/wiki/BBCode). + +use markdown::mdast::Node; +use markdown::{to_mdast, ParseOptions}; +use std::collections::HashMap; + +pub fn to_bbcode(md: &str) -> String { + // to_mdast() never errors with normal arkdown, so unwrap is safe. + let n = to_mdast(md, &ParseOptions::gfm()).unwrap(); + let definitions = n + .children() + .unwrap() // root node always has children + .iter() + .filter_map(|n| match n { + Node::Definition(definition) => Some((&*definition.identifier, &*definition.url)), + _ => None, + }) + .collect::>(); + + walk_node(&n, &definitions).unwrap_or_default() +} + +fn walk_node(node: &Node, definitions: &HashMap<&str, &str>) -> Option { + use Node::*; + let bbcode = match node { + Root(root) => walk_nodes(&root.children, definitions, "[br][br]"), + InlineCode(markdown::mdast::InlineCode { value, .. }) => format!("[code]{value}[/code]"), + Delete(delete) => format!("[s]{}[/s]", walk_nodes(&delete.children, definitions, "")), + Emphasis(emphasis) => format!("[i]{}[/i]", walk_nodes(&emphasis.children, definitions, "")), + Image(markdown::mdast::Image { url, .. }) => format!("[img]{url}[/img]",), + ImageReference(image) => { + format!( + "[img]{}[/img]", + definitions.get(&&*image.identifier).unwrap() + ) + } + Link(markdown::mdast::Link { url, children, .. }) => { + format!("[url={url}]{}[/url]", walk_nodes(children, definitions, "")) + } + LinkReference(markdown::mdast::LinkReference { + identifier, + children, + .. + }) => format!( + "[url={}]{}[/url]", + definitions.get(&&**identifier).unwrap(), + walk_nodes(children, definitions, "") + ), + Strong(strong) => format!("[b]{}[/b]", walk_nodes(&strong.children, definitions, "")), + Text(text) => text.value.clone(), + // TODO: more langs? + Code(markdown::mdast::Code { value, .. }) => format!("[codeblock]{value}[/codeblock]"), + Paragraph(paragraph) => walk_nodes(¶graph.children, definitions, ""), + // bbcode supports lists but docs dont + List(_) | BlockQuote(_) | FootnoteReference(_) | FootnoteDefinition(_) | Table(_) => { + "".into() + } + _ => walk_nodes(&node.children()?, definitions, ""), + }; + Some(bbcode) +} + +/// Calls [`walk_node`] over every node its given, joining them with the supplied separator. +fn walk_nodes(nodes: &[Node], definitions: &HashMap<&str, &str>, separator: &str) -> String { + nodes + .iter() + .filter_map(|n| walk_node(n, definitions)) + .collect::>() + .join(separator) +} diff --git a/godot-macros/src/lib.rs b/godot-macros/src/lib.rs index 836e2e6d0..abea64678 100644 --- a/godot-macros/src/lib.rs +++ b/godot-macros/src/lib.rs @@ -13,6 +13,8 @@ mod bench; mod class; mod derive; +#[cfg(all(feature = "docs", since_api = "4.3"))] +mod docs; mod gdextension; mod itest; mod util; @@ -174,7 +176,7 @@ use crate::util::ident; /// ``` /// # use godot::prelude::*; /// #[derive(GodotClass)] -/// # #[class(init)] +/// #[class(init)] /// struct MyStruct { /// #[var(get = get_my_field, set = set_my_field)] /// my_field: i64, @@ -444,6 +446,35 @@ use crate::util::ident; /// # fn init(base: godot::obj::Base) -> Self { todo!() } /// # } /// ``` +/// +///
Available on crate feature register-docs only.
+///
Available on Godot version 4.3+ only.
+/// +/// # Documentation +/// +/// You can document your functions, classes, members, and signals with the `///` doc comment syntax. +/// +/// ```no_run +/// # use godot::prelude::*; +/// #[derive(GodotClass)] +/// # #[class(init)] +/// /// This is an example struct for documentation, inside documentation. +/// struct DocumentedStruct { +/// /// This is a class member. +/// /// You can use markdown formatting such as _italics_. +/// #[var] +/// item: f32, +/// } +/// +/// #[godot_api] +/// impl DocumentedStruct { +/// /// This provides the item, after adding `0.2`. +/// #[func] +/// pub fn produce_item(&self) -> f32 { +/// self.item + 0.2 +/// } +/// } +/// ``` #[proc_macro_derive(GodotClass, attributes(class, base, hint, var, export, init, signal))] pub fn derive_godot_class(input: TokenStream) -> TokenStream { translate(input, class::derive_godot_class) diff --git a/godot/Cargo.toml b/godot/Cargo.toml index 4434d28a6..e987b2a35 100644 --- a/godot/Cargo.toml +++ b/godot/Cargo.toml @@ -24,6 +24,8 @@ codegen-rustfmt = ["godot-core/codegen-rustfmt"] lazy-function-tables = ["godot-core/codegen-lazy-fptrs"] serde = ["godot-core/serde"] +register-docs = ["godot-macros/docs", "godot-core/docs"] + api-custom = ["godot-core/api-custom"] # [version-sync] [[ # [line] api-$kebabVersion = ["godot-core/api-$kebabVersion"] diff --git a/godot/src/lib.rs b/godot/src/lib.rs index ee0e2c483..df2ced805 100644 --- a/godot/src/lib.rs +++ b/godot/src/lib.rs @@ -107,6 +107,13 @@ //! Use rustfmt to format generated binding code. Because rustfmt is so slow, this is detrimental to initial compile time. //! Without it, we use a lightweight and fast custom formatter to enable basic human readability. //! +//! * **`register-docs`** +//! +//! Generates documentation for your structs from your Rust documentation. +//! Documentation is visible in Godot via `F1` -> searching for that class. +//! This feature requires at least Godot 4.3. +//! See also: [`#[derive(GodotClass)]`](register/derive.GodotClass.html#documentation) +//! //! _Integrations:_ //! //! * **`serde`** @@ -171,6 +178,9 @@ pub use godot_core::{builtin, classes, global, meta, obj, tools}; #[allow(deprecated)] pub use godot_core::{engine, log}; +#[doc(hidden)] +pub use godot_core::possibly_docs as docs; + #[doc(hidden)] pub use godot_core::sys; diff --git a/godot/tests/docs.rs b/godot/tests/docs.rs new file mode 100644 index 000000000..ae99bec91 --- /dev/null +++ b/godot/tests/docs.rs @@ -0,0 +1,131 @@ +/* + * Copyright (c) godot-rust; Bromeon and contributors. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +use godot::prelude::*; + +/// *documented* ~ **documented** ~ [AABB] [pr](https://github.com/godot-rust/gdext/pull/748) +/// +/// a few tests: +/// +/// headings: +/// +/// # Some heading +/// +/// lists: +/// +/// - lists +/// - like this +/// * maybe with `*` as well +/// +/// links with back-references: +/// +/// Blah blah [^foo] +/// [^foo]: https://example.org +/// +/// footnotes: +/// +/// We cannot florbinate the glorb[^florb] +/// [^florb]: because the glorb doesn't flibble. +/// +/// task lists: +/// +/// We must ensure that we've completed +/// - [ ] task 1 +/// - [x] task 2 +/// +/// tables: +/// +/// | Header1 | Header2 | +/// |---------|---------| +/// | abc | def | +/// +/// images: +/// +/// ![Image](http://url/a.png) +/// +/// blockquotes: +/// +/// > Some cool thing +/// +/// ordered list: +/// +/// 1. thing one +/// 2. thing two +/// +/// +/// Something here < this is technically header syntax +/// --- +/// And here +/// +/// smart punctuation +/// +/// codeblocks: +/// +/// ```rust +/// #![no_main] +/// #[link_section=".text"] +/// #[no_mangle] +/// static main: u64 = 0x31c0678b10; +/// ``` +/// +/// connect +/// these +#[derive(GodotClass)] +#[class(base=Node)] +pub struct ExtremelyDocumented { + #[doc = r#"this is very documented"#] + #[var] + item: f32, + /// this isnt documented + _other_item: (), + /// nor this + base: Base, +} + +#[godot_api] +impl INode for ExtremelyDocumented { + /// initialize this + fn init(base: Base) -> Self { + Self { + base, + item: 883.0, + _other_item: {}, + } + } +} + +#[godot_api] +impl ExtremelyDocumented { + #[constant] + /// Documentation. + const RANDOM: i64 = 4; + + #[func] + /// huh + fn ye(&self) -> f32 { + self.item + } + + #[func] + /// wow + fn ne(_x: f32) -> Gd { + panic!() + } +} + +#[test] +#[cfg(feature = "register-docs")] +fn correct() { + // Uncomment if implementation changes and expected output file should be rewritten. + // std::fs::write( + // "tests/docs.xml", + // godot_core::docs::gather_xml_docs().next().unwrap(), + // ); + assert_eq!( + include_str!("docs.xml"), + godot_core::docs::gather_xml_docs().next().unwrap() + ); +} diff --git a/godot/tests/docs.xml b/godot/tests/docs.xml new file mode 100644 index 000000000..fd9f4e92f --- /dev/null +++ b/godot/tests/docs.xml @@ -0,0 +1,38 @@ + + + +[i]documented[/i] ~ [b]documented[/b] ~ [AABB] [url=https://github.com/godot-rust/gdext/pull/748]pr[/url] +[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] +#[link_section=\".text\"] +#[no_mangle] +static main: u64 = 0x31c0678b10;[/codeblock][br][br]connect +these + + + + + + huh + + + + + + + + wow + + + + + + + + initialize this + + + +Documentation. + +this is very documented + \ No newline at end of file