Skip to main content

pretty_text_parser/
syntax.rs

1//! Module for generating parser documentation.
2//!
3//! [`DynamicEffectSyntax`] contains documentation and usage examples for
4//! generating rustdoc at compile time and providing helpful error reports
5//! at run time.
6
7use bevy::ecs::reflect::AppTypeRegistry;
8use bevy::reflect::{Reflect, reflect_trait};
9use convert_case::Casing;
10
11/// Provides documentation at run-time for a dynamic effect.
12///
13/// [`DynamicEffectSyntax`] is generated automatically for built-in effects but
14/// the mechanism is not exposed.
15#[reflect_trait]
16pub trait GetDynamicEffectSyntax {
17    /// Get the [`DynamicEffectSyntax`] for this type.
18    fn get(&self) -> DynamicEffectSyntax<'static>;
19}
20
21/// Convenience function for retrieving [`DynamicEffectSyntax`] from a [`Reflect`]
22/// type which may or may not implement [`GetDynamicEffectSyntax`].
23pub fn get_dynamic_effect_syntax<T: Reflect>(
24    registry: &AppTypeRegistry,
25    effect: &T,
26) -> Option<DynamicEffectSyntax<'static>> {
27    let registry = registry.read();
28    let registration = registry.get(std::any::TypeId::of::<T>())?;
29    let dynamic_syntax = registration.data::<ReflectGetDynamicEffectSyntax>()?;
30    dynamic_syntax
31        .get(effect.as_reflect())
32        .map(GetDynamicEffectSyntax::get)
33}
34
35/// Syntax documentation for a dynamic effect.
36#[derive(Debug, Clone, Copy)]
37pub struct DynamicEffectSyntax<'a> {
38    /// The tag of the effect.
39    pub name: &'a str,
40    /// All of the fields exposed to the dynamic effect.
41    /// implementation.
42    pub fields: &'a [DynamicEffectFieldSyntax<'a>],
43    /// Flag for whether or not this is a glyph material.
44    pub is_material: bool,
45}
46
47impl<'a> DynamicEffectSyntax<'a> {
48    /// Generate help string for this effect.
49    pub fn help_fmt(&self) -> String {
50        format!(
51            "Usage: pretty!(\"[my text]({}({}))\")\nSyntax for `{}`:\n{}",
52            self.name,
53            self.parsed_field_usage(),
54            self.name,
55            self.fields
56                .iter()
57                .map(DynamicEffectFieldSyntax::help_fmt)
58                .collect::<Vec<_>>()
59                .join("\n"),
60        )
61    }
62
63    /// Generate rustdoc with syntax and usage.
64    pub fn doc_fmt(&self) -> String {
65        format!(
66            "# [Argument Syntax](pretty_text::parser)\n{}\n{}\n{}",
67            self.fields
68                .iter()
69                .map(DynamicEffectFieldSyntax::doc_fmt)
70                .collect::<Vec<_>>()
71                .join("\n\n"),
72            self.usage(),
73            NOTES,
74        )
75    }
76
77    fn usage(&self) -> String {
78        let insert = if self.is_material {
79            format!(
80                "PrettyTextMaterial(materials.add({}::default()))",
81                self.name.to_case(convert_case::Case::Pascal),
82            )
83        } else {
84            format!(
85                "{}::default()",
86                self.name.to_case(convert_case::Case::Pascal),
87            )
88        };
89
90        format!(
91            "{0}\n// Parsed\nworld.spawn(pretty!(\"[my text]({1}({2}))\"));\n\
92                world.spawn(PrettyParser::bundle(\"[my text]({1}({2}))\")?);\n\n\
93                // Literal\nworld.spawn((\n\tText::new(\"my text\"),\n\t{3},\n));\n{4}",
94            USAGE_HEADER_STR,
95            self.name,
96            self.parsed_field_usage(),
97            insert,
98            USAGE_FOOTER_STR,
99        )
100    }
101
102    fn parsed_field_usage(&self) -> String {
103        self.fields
104            .iter()
105            .map(|field| {
106                let dot_stripped = field.default.strip_suffix(".0").unwrap_or(field.default);
107                field.default.strip_suffix("f32").unwrap_or(dot_stripped)
108            })
109            .collect::<Vec<_>>()
110            .join(", ")
111    }
112}
113
114/// Syntax documentation for a dynamic effect field.
115#[derive(Debug, Clone, Copy)]
116pub struct DynamicEffectFieldSyntax<'a> {
117    /// Field identifier.
118    pub name: &'a str,
119    /// Field rustdoc.
120    pub docs: &'a str,
121    /// The default value.
122    pub default: &'a str,
123    /// A collection of valid arguments and optional shorthand.
124    pub arguments: &'a [Argument<'a>],
125}
126
127impl DynamicEffectFieldSyntax<'_> {
128    /// Generate help string for this field.
129    pub fn help_fmt(&self) -> String {
130        format!(
131            "  {}: {}\n    Valid arguments: [{}]",
132            self.name,
133            self.docs,
134            self.arguments
135                .iter()
136                .map(Argument::help_fmt)
137                .collect::<Vec<_>>()
138                .join(", ")
139        )
140    }
141
142    /// Generate rustdoc for this field.
143    pub fn doc_fmt(&self) -> String {
144        format!(
145            "`{}`: {}\n{}",
146            self.name,
147            self.docs,
148            self.arguments
149                .iter()
150                .map(Argument::doc_fmt)
151                .collect::<Vec<_>>()
152                .join("")
153        )
154    }
155}
156
157/// Argument in [`DynamicEffectFieldSyntax`].
158#[derive(Debug, Clone, Copy)]
159pub struct Argument<'a> {
160    /// The `base` argument that `shorthand` expands to.
161    pub base: &'a str,
162    /// The shorthand syntax.
163    pub shorthand: Option<&'a str>,
164}
165
166impl Argument<'_> {
167    /// Generate syntax for this argument.
168    pub fn help_fmt(&self) -> String {
169        match self.shorthand {
170            Some(shorthand) => format!("`\"{}\"` or `\"{}\"`", self.base, shorthand),
171            None => format!("`\"{}\"`", self.base),
172        }
173    }
174
175    /// Generate rustdoc for this argument.
176    pub fn doc_fmt(&self) -> String {
177        match self.shorthand {
178            Some(shorthand) => format!("- `\"{}\"`, `\"{}\"`\n", self.base, shorthand),
179            None => format!("- `\"{}\"`\n", self.base),
180        }
181    }
182}
183
184// rustdoc wrappers around the usage examples for syntax documentation
185const USAGE_HEADER_STR: &str = r"
186# Usage
187
188```
189# use bevy_pretty_text::prelude::*;
190# use bevy::prelude::*;
191#
192# struct DummyMaterials;
193# impl DummyMaterials {
194#     fn add<T: bevy::prelude::Asset>(&self, _: T) -> bevy::prelude::Handle<T> {
195#         bevy::prelude::Handle::default()
196#     }
197# }
198#
199# fn __test() -> bevy::prelude::Result {
200#     let mut world = bevy::prelude::World::new();
201#     #[allow(unused)]
202#     let materials = DummyMaterials;";
203
204const USAGE_FOOTER_STR: &str = r"
205#     Ok(())
206# }
207# assert!(__test().is_ok());
208```";
209
210const NOTES: &str = r#"
211### Notes
212- See [`ArgParser`](crate::parser::ArgParser) for documentation on argument syntax.\n
213- Positional arguments must appear in order.\n
214- Named arguments can appear after positional arguments but not before.\n
215"#;
216
217#[cfg(feature = "proc-macro")]
218impl quote::ToTokens for DynamicEffectSyntax<'_> {
219    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
220        use quote::TokenStreamExt;
221
222        let name = self.name;
223        let fields = self.fields;
224        let is_material = self.is_material;
225
226        tokens.append_all(quote::quote! {
227            ::bevy_pretty_text::effects::dynamic::DynamicEffectSyntax {
228                name: #name,
229                fields: &[#(#fields,)*],
230                is_material: #is_material,
231            }
232        });
233    }
234}
235
236#[cfg(feature = "proc-macro")]
237impl quote::ToTokens for DynamicEffectFieldSyntax<'_> {
238    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
239        use quote::TokenStreamExt;
240
241        let name = self.name;
242        let docs = self.docs;
243        let default = self.default;
244        let arguments = self.arguments;
245
246        tokens.append_all(quote::quote! {
247            ::bevy_pretty_text::effects::dynamic::DynamicEffectFieldSyntax {
248                name: #name,
249                docs: #docs,
250                default: #default,
251                arguments: &[#(#arguments,)*],
252            }
253        });
254    }
255}
256
257#[cfg(feature = "proc-macro")]
258impl quote::ToTokens for Argument<'_> {
259    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
260        use quote::{TokenStreamExt, quote};
261
262        let base = self.base;
263        let shorthand = match self.shorthand {
264            Some(shorthand) => quote! { Some(#shorthand) },
265            None => quote! { None },
266        };
267
268        tokens.append_all(quote! {
269            ::bevy_pretty_text::effects::dynamic::Argument {
270                base: #base,
271                shorthand: #shorthand,
272            }
273        });
274    }
275}