diff --git a/Cargo.toml b/Cargo.toml index 9f17f79990..ee94dddfd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,11 @@ exclude = ["crates/bevy_api_gen", "crates/macro_tests"] debug = 1 opt-level = 1 +[profile.dev-debug] +inherits = "dev" +debug = true +opt-level = 0 + [profile.dev.package."*"] debug = 0 opt-level = 3 diff --git a/crates/bevy_mod_scripting_core/src/docgen/info.rs b/crates/bevy_mod_scripting_core/src/docgen/info.rs index 4fa182b900..e0b951dc1c 100644 --- a/crates/bevy_mod_scripting_core/src/docgen/info.rs +++ b/crates/bevy_mod_scripting_core/src/docgen/info.rs @@ -129,11 +129,14 @@ impl FunctionArgInfo { } } -#[derive(Debug, Clone, PartialEq, Reflect)] +#[derive(Debug, Clone, Reflect)] /// Information about a function return value. pub struct FunctionReturnInfo { /// The type of the return value. pub type_id: TypeId, + /// The type information of the return value. + #[reflect(ignore)] + pub type_info: Option, } impl Default for FunctionReturnInfo { @@ -144,9 +147,10 @@ impl Default for FunctionReturnInfo { impl FunctionReturnInfo { /// Create a new function return info for a specific type. - pub fn new_for() -> Self { + pub fn new_for() -> Self { Self { type_id: TypeId::of::(), + type_info: Some(T::through_type_info()), } } } diff --git a/crates/lad_backends/mdbook_lad_preprocessor/src/argument_visitor.rs b/crates/lad_backends/mdbook_lad_preprocessor/src/argument_visitor.rs new file mode 100644 index 0000000000..db7eefa738 --- /dev/null +++ b/crates/lad_backends/mdbook_lad_preprocessor/src/argument_visitor.rs @@ -0,0 +1,233 @@ +//! Defines a visitor for function arguments of the `LAD` format. + +use ladfile::ArgumentVisitor; + +use crate::markdown::MarkdownBuilder; + +pub(crate) struct MarkdownArgumentVisitor<'a> { + ladfile: &'a ladfile::LadFile, + buffer: MarkdownBuilder, +} +impl<'a> MarkdownArgumentVisitor<'a> { + pub fn new(ladfile: &'a ladfile::LadFile) -> Self { + let mut builder = MarkdownBuilder::new(); + builder.tight_inline().set_escape_mode(false); + Self { + ladfile, + buffer: builder, + } + } + + pub fn build(mut self) -> String { + self.buffer.build() + } +} + +impl ArgumentVisitor for MarkdownArgumentVisitor<'_> { + fn visit_lad_type_id(&mut self, type_id: &ladfile::LadTypeId) { + let mut buffer = String::new(); + + // Write identifier + buffer.push_str(&self.ladfile.get_type_identifier(type_id)); + if let Some(generics) = self.ladfile.get_type_generics(type_id) { + buffer.push('<'); + for (i, generic) in generics.iter().enumerate() { + if i > 0 { + buffer.push_str(", "); + } + buffer.push_str(&self.ladfile.get_type_identifier(&generic.type_id)); + } + buffer.push('>'); + } + + self.buffer.text(buffer); + } + + fn walk_option(&mut self, inner: &ladfile::LadArgumentKind) { + // Write Optional + self.buffer.text("Optional<"); + self.visit(inner); + self.buffer.text(">"); + } + + fn walk_vec(&mut self, inner: &ladfile::LadArgumentKind) { + // Write Vec + self.buffer.text("Vec<"); + self.visit(inner); + self.buffer.text(">"); + } + + fn walk_hash_map(&mut self, key: &ladfile::LadArgumentKind, value: &ladfile::LadArgumentKind) { + // Write HashMap + self.buffer.text("HashMap<"); + self.visit(key); + self.buffer.text(", "); + self.visit(value); + self.buffer.text(">"); + } + + fn walk_tuple(&mut self, inner: &[ladfile::LadArgumentKind]) { + // Write (inner1, inner2, ...) + self.buffer.text("("); + for (idx, arg) in inner.iter().enumerate() { + self.visit(arg); + if idx < inner.len() - 1 { + self.buffer.text(", "); + } + } + self.buffer.text(")"); + } + + fn walk_array(&mut self, inner: &ladfile::LadArgumentKind, size: usize) { + // Write [inner; size] + self.buffer.text("["); + self.visit(inner); + self.buffer.text("; "); + self.buffer.text(size.to_string()); + self.buffer.text("]"); + } +} + +#[cfg(test)] +mod test { + use ladfile::LadArgumentKind; + + use super::*; + + fn setup_ladfile() -> ladfile::LadFile { + // load test file from ../../../ladfile_builder/test_assets/ + let ladfile = ladfile::EXAMPLE_LADFILE; + ladfile::parse_lad_file(ladfile).unwrap() + } + + #[test] + fn test_visit_type_id() { + let ladfile = setup_ladfile(); + + let first_type_id = ladfile.types.first().unwrap().0; + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit_lad_type_id(first_type_id); + assert_eq!(visitor.buffer.build(), "EnumType"); + + visitor.buffer.clear(); + + let second_type_id = ladfile.types.iter().nth(1).unwrap().0; + visitor.visit_lad_type_id(second_type_id); + assert_eq!(visitor.buffer.build(), "StructType"); + } + + #[test] + fn test_visit_ref() { + let ladfile = setup_ladfile(); + + let first_type_id = ladfile.types.first().unwrap().0; + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::Ref(first_type_id.clone())); + assert_eq!(visitor.buffer.build(), "EnumType"); + } + + #[test] + fn test_visit_mut() { + let ladfile = setup_ladfile(); + + let first_type_id = ladfile.types.first().unwrap().0; + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::Mut(first_type_id.clone())); + assert_eq!(visitor.buffer.build(), "EnumType"); + } + + #[test] + fn test_visit_val() { + let ladfile = setup_ladfile(); + + let first_type_id = ladfile.types.first().unwrap().0; + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::Val(first_type_id.clone())); + assert_eq!(visitor.buffer.build(), "EnumType"); + } + + #[test] + fn test_visit_option() { + let ladfile = setup_ladfile(); + + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::Option(Box::new( + LadArgumentKind::Primitive(ladfile::LadBMSPrimitiveKind::Bool), + ))); + assert_eq!(visitor.buffer.build(), "Optional"); + } + + #[test] + fn test_visit_vec() { + let ladfile = setup_ladfile(); + + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::Vec(Box::new(LadArgumentKind::Primitive( + ladfile::LadBMSPrimitiveKind::Bool, + )))); + assert_eq!(visitor.buffer.build(), "Vec"); + } + + #[test] + fn test_visit_hash_map() { + let ladfile = setup_ladfile(); + + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::HashMap( + Box::new(LadArgumentKind::Primitive( + ladfile::LadBMSPrimitiveKind::Bool, + )), + Box::new(LadArgumentKind::Primitive( + ladfile::LadBMSPrimitiveKind::String, + )), + )); + assert_eq!(visitor.buffer.build(), "HashMap"); + } + + #[test] + fn test_visit_tuple() { + let ladfile = setup_ladfile(); + + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::Tuple(vec![ + LadArgumentKind::Primitive(ladfile::LadBMSPrimitiveKind::Bool), + LadArgumentKind::Primitive(ladfile::LadBMSPrimitiveKind::String), + ])); + assert_eq!(visitor.buffer.build(), "(bool, String)"); + } + + #[test] + fn test_visit_array() { + let ladfile = setup_ladfile(); + + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + visitor.visit(&LadArgumentKind::Array( + Box::new(LadArgumentKind::Primitive( + ladfile::LadBMSPrimitiveKind::Bool, + )), + 5, + )); + assert_eq!(visitor.buffer.build(), "[bool; 5]"); + } + + #[test] + fn test_visit_unknown() { + let ladfile = setup_ladfile(); + + let mut visitor = MarkdownArgumentVisitor::new(&ladfile); + + let first_type_id = ladfile.types.first().unwrap().0; + + visitor.visit(&LadArgumentKind::Unknown(first_type_id.clone())); + assert_eq!(visitor.buffer.build(), "EnumType"); + } +} diff --git a/crates/lad_backends/mdbook_lad_preprocessor/src/lib.rs b/crates/lad_backends/mdbook_lad_preprocessor/src/lib.rs index 6b0eb39c5d..8ca2b16c81 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/src/lib.rs +++ b/crates/lad_backends/mdbook_lad_preprocessor/src/lib.rs @@ -2,6 +2,7 @@ #![allow(missing_docs)] use mdbook::{errors::Error, preprocess::Preprocessor}; +mod argument_visitor; mod markdown; mod sections; @@ -31,6 +32,10 @@ impl Preprocessor for LADPreprocessor { if !is_lad_chapter { log::debug!("Skipping non-LAD chapter: {:?}", chapter.source_path); + log::trace!( + "Non-LAD chapter: {}", + serde_json::to_string_pretty(&chapter).unwrap_or_default() + ); return; } @@ -45,7 +50,10 @@ impl Preprocessor for LADPreprocessor { } }; - log::debug!("Parsed LAD file: {:?}", lad); + log::debug!( + "Parsed LAD file: {}", + serde_json::to_string_pretty(&lad).unwrap_or_default() + ); let sections = sections::lad_file_to_sections(&lad, Some(chapter_title)); @@ -58,7 +66,11 @@ impl Preprocessor for LADPreprocessor { None, ); - log::debug!("New chapter: {:?}", new_chapter); + // serialize chapter to json + log::debug!( + "New chapter: {}", + serde_json::to_string_pretty(&new_chapter).unwrap_or_default() + ); *chapter = new_chapter; } diff --git a/crates/lad_backends/mdbook_lad_preprocessor/src/markdown.rs b/crates/lad_backends/mdbook_lad_preprocessor/src/markdown.rs index 00afc71f8b..68a388c023 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/src/markdown.rs +++ b/crates/lad_backends/mdbook_lad_preprocessor/src/markdown.rs @@ -1,7 +1,45 @@ use std::borrow::Cow; +/// Takes the first n characters from the markdown, without splitting any formatting +pub(crate) fn markdown_substring(markdown: &str, length: usize) -> &str { + if markdown.len() <= length { + return markdown; + } + let mut end = length; + for &(open, close) in &[("`", "`"), ("**", "**"), ("*", "*"), ("_", "_"), ("[", "]")] { + // Count markers in the already cut substring. + let count = markdown[..end].matches(open).count(); + // Check if an opening marker starts right at the cutoff. + let extra = if markdown[end..].starts_with(open) { + 1 + } else { + 0 + }; + if (count + extra) % 2 == 1 { + let search_start = if extra == 1 { end + open.len() } else { end }; + if let Some(pos) = markdown[search_start..].find(close) { + end = search_start + pos + close.len(); + // Special handling for links: if the marker is "[" then check if a '(' follows. + if open == "[" && markdown.len() > end && markdown[end..].starts_with('(') { + let paren_search_start = end + 1; + if let Some(paren_pos) = markdown[paren_search_start..].find(')') { + end = paren_search_start + paren_pos + 1; + } + } + } else { + return markdown; + } + } + } + &markdown[..end] +} + /// Escapes Markdown reserved characters in the given text. -fn escape_markdown(text: &str) -> String { +fn escape_markdown(text: &str, escape: bool) -> String { + if !escape { + return text.to_string(); + } + // Characters that should be escaped in markdown let escape_chars = r"\`*_{}[]()#+-.!"; let mut escaped = String::with_capacity(text.len()); @@ -56,6 +94,9 @@ pub enum Markdown { headers: Vec, rows: Vec>, }, + Raw { + text: String, + }, } #[allow(dead_code)] @@ -114,7 +155,7 @@ impl IntoMarkdown for Markdown { let clamped_level = level.clamp(&1, &6); let hashes = "#".repeat(*clamped_level as usize); // Escape the text for Markdown - builder.append(&format!("{} {}", hashes, escape_markdown(text))); + builder.append(&format!("{} {}", hashes, text)); } Markdown::Paragraph { text, @@ -135,7 +176,7 @@ impl IntoMarkdown for Markdown { let escaped = if *code { text.clone() } else { - escape_markdown(text) + escape_markdown(text, builder.escape) }; builder.append(&escaped); @@ -173,14 +214,18 @@ impl IntoMarkdown for Markdown { Markdown::Quote(text) => { let quote_output = text .lines() - .map(|line| format!("> {}", escape_markdown(line))) + .map(|line| format!("> {}", line)) .collect::>() .join("\n"); builder.append("e_output); } Markdown::Image { alt, src } => { // Escape alt text while leaving src untouched. - builder.append(&format!("![{}]({})", escape_markdown(alt), src)); + builder.append(&format!( + "![{}]({})", + escape_markdown(alt, builder.escape), + src + )); } Markdown::Link { text, url, anchor } => { // anchors must be lowercase, only contain letters or dashes @@ -196,12 +241,20 @@ impl IntoMarkdown for Markdown { url.clone() }; // Escape link text while leaving url untouched. - builder.append(&format!("[{}]({})", escape_markdown(text), url)); + builder.append(&format!( + "[{}]({})", + escape_markdown(text, builder.escape), + url + )); } Markdown::HorizontalRule => { builder.append("---"); } Markdown::Table { headers, rows } => { + if rows.is_empty() { + return; + } + // Generate a Markdown table: // Header row: let header_line = format!("| {} |", headers.join(" | ")); @@ -225,25 +278,28 @@ impl IntoMarkdown for Markdown { header_line, separator_line, rows_lines )); } + Markdown::Raw { text } => { + builder.append(text); + } } } } impl IntoMarkdown for &str { fn to_markdown(&self, builder: &mut MarkdownBuilder) { - builder.append(&escape_markdown(self)) + builder.append(&escape_markdown(self, builder.escape)) } } impl IntoMarkdown for String { fn to_markdown(&self, builder: &mut MarkdownBuilder) { - builder.append(&escape_markdown(self.as_ref())) + builder.append(&escape_markdown(self.as_ref(), builder.escape)) } } impl IntoMarkdown for Cow<'_, str> { fn to_markdown(&self, builder: &mut MarkdownBuilder) { - builder.append(&escape_markdown(self.as_ref())) + builder.append(&escape_markdown(self.as_ref(), builder.escape)) } } @@ -270,7 +326,7 @@ impl IntoMarkdown for Vec { item.to_markdown(builder); if i < self.len() - 1 { if builder.inline { - builder.append(" "); + builder.append(builder.inline_separator); } else { builder.append("\n\n"); } @@ -281,28 +337,57 @@ impl IntoMarkdown for Vec { /// Builder pattern for generating comprehensive Markdown documentation. /// Now also doubles as the accumulator for the generated markdown. +#[derive(Clone)] pub struct MarkdownBuilder { elements: Vec, output: String, - inline: bool, + pub inline: bool, + pub inline_separator: &'static str, + pub escape: bool, } #[allow(dead_code)] impl MarkdownBuilder { + /// Clears the builder's buffer + pub fn clear(&mut self) { + self.elements.clear(); + self.output.clear(); + } + /// Creates a new MarkdownBuilder. pub fn new() -> Self { MarkdownBuilder { elements: Vec::new(), output: String::new(), inline: false, + inline_separator: " ", + escape: true, } } + /// Disables or enables the automatic escaping of Markdown reserved characters. + /// by default it is enabled. + /// + /// Will only affect elements which are escaped by default such as text. + pub fn set_escape_mode(&mut self, escape: bool) -> &mut Self { + self.escape = escape; + self + } + + /// Enables inline mode, which prevents newlines from being inserted for elements that support it pub fn inline(&mut self) -> &mut Self { self.inline = true; self } + /// Enables inline mode on top of disabling the automatic space separator. + /// Each element will simply be concatenated without any separator. + pub fn tight_inline(&mut self) -> &mut Self { + self.inline = true; + self.inline_separator = ""; + self + } + /// Adds an in-place slot for more complex markdown generation while preserving the builder flow. pub fn complex(&mut self, f: impl FnOnce(&mut MarkdownBuilder)) -> &mut Self { f(self); @@ -315,10 +400,15 @@ impl MarkdownBuilder { } /// Adds a heading element (Levels from 1-6). - pub fn heading(&mut self, level: u8, text: impl Into) -> &mut Self { + pub fn heading(&mut self, level: u8, text: impl IntoMarkdown) -> &mut Self { + let mut builder = MarkdownBuilder::new(); + builder.inline(); + text.to_markdown(&mut builder); + let text = builder.build(); + self.elements.push(Markdown::Heading { level: level.min(6), - text: text.into(), + text, }); self } @@ -400,8 +490,10 @@ impl MarkdownBuilder { } /// Adds a quote element. - pub fn quote(&mut self, text: impl Into) -> &mut Self { - self.elements.push(Markdown::Quote(text.into())); + pub fn quote(&mut self, text: impl IntoMarkdown) -> &mut Self { + let mut builder = MarkdownBuilder::new(); + text.to_markdown(&mut builder); + self.elements.push(Markdown::Quote(builder.build())); self } @@ -455,7 +547,7 @@ impl MarkdownBuilder { element.to_markdown(self); if i < len - 1 { if self.inline { - self.append(" "); + self.append(self.inline_separator); } else { self.append("\n\n"); } @@ -465,6 +557,12 @@ impl MarkdownBuilder { } } +impl IntoMarkdown for MarkdownBuilder { + fn to_markdown(&self, builder: &mut MarkdownBuilder) { + *builder = self.clone() + } +} + /// Mini builder for constructing Markdown tables. pub struct TableBuilder { headers: Vec, @@ -600,4 +698,29 @@ mod tests { pretty_assertions::assert_eq!(trimmed_indentation_expected, trimmed_indentation_markdown); } + + #[test] + fn test_markdown_substring_works() { + // Test markdown_substring with simple 5–7 character inputs. + let cases = vec![ + // Inline code: "a`bcd`" → with len 3, substring "a`b" is extended to the full inline segment. + ("a`bcd`", 3, "a`bcd`"), + // Bold: "a**b**" → with len 3, substring "a**" is extended to "a**b**". + ("a**b**", 3, "a**b**"), + // Italic: "a*b*" → with len 1, substring "["a*", extended to "a*b*". + ("a*b*", 1, "a*b*"), + // Underscore: "a_b_" → with len 1, extended to "a_b_". + ("a_b_", 1, "a_b_"), + // Link-like: "[x](y)" → with len 1, extended to the next closing bracket. + ("[x](y)", 1, "[x](y)"), + ]; + for (input, len, expected) in cases { + assert_eq!( + expected, + markdown_substring(input, len), + "Failed for input: {}", + input + ); + } + } } diff --git a/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs b/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs index 05ae6d4989..5828b849e9 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs +++ b/crates/lad_backends/mdbook_lad_preprocessor/src/sections.rs @@ -1,12 +1,13 @@ -use std::{borrow::Cow, path::PathBuf}; - -use ladfile::{LadFunction, LadInstance, LadType, LadTypeLayout}; -use mdbook::book::{Chapter, SectionNumber}; - use crate::{ - markdown::{IntoMarkdown, Markdown, MarkdownBuilder}, + argument_visitor::MarkdownArgumentVisitor, + markdown::{markdown_substring, IntoMarkdown, Markdown, MarkdownBuilder}, markdown_vec, }; +use ladfile::{ + ArgumentVisitor, LadArgument, LadFile, LadFunction, LadInstance, LadType, LadTypeLayout, +}; +use mdbook::book::{Chapter, SectionNumber}; +use std::{borrow::Cow, collections::HashSet, path::PathBuf}; pub(crate) fn section_to_chapter( section: SectionAndChildren, @@ -19,12 +20,18 @@ pub(crate) fn section_to_chapter( let mut parent_builder = MarkdownBuilder::new(); section.section.to_markdown(&mut parent_builder); + // important to reset the extension of the parent, since when we're nesting + // we add the filename with .md, but if the parent is being emitted as markdown, then when + // we create the child, we will create the `parent.md` file as a folder, then when we emit + // the parent itself, the file (directory) will already exist let new_path = root_path .unwrap_or_default() + .with_extension("") .join(section.section.file_name()); let new_source_path = root_source_path .unwrap_or_default() + .with_extension("") .join(section.section.file_name()); let current_number = number.clone().unwrap_or_default(); @@ -79,26 +86,66 @@ pub(crate) fn section_to_chapter( } } +fn section_to_section_and_children(section: Section<'_>) -> SectionAndChildren<'_> { + let children = section + .children() + .into_iter() + .map(section_to_section_and_children) + .collect(); + + SectionAndChildren { children, section } +} + pub(crate) fn lad_file_to_sections( ladfile: &ladfile::LadFile, title: Option, ) -> SectionAndChildren<'_> { - let summary = Section::Summary { ladfile, title }; - - let children = ladfile - .types - .iter() - .map(|(_, lad_type)| Section::TypeDetail { lad_type, ladfile }) - .map(|section| SectionAndChildren { - section, - children: Vec::new(), - }) - .collect(); + section_to_section_and_children(Section::Summary { ladfile, title }) + // build a hierarchy as follows: + // - Summary + // - Instances + // - Functions + // - Global Function Detail 1 + // - Types + // - Type1 + // - Type detail 1 + // - Function detail 1 + // - Function detail 2 + // let mut types_children = ladfile + // .types + // .iter() + // .map(|(_, lad_type)| (lad_type, Section::TypeDetail { lad_type, ladfile })) + // .map(|(lad_type, section)| SectionAndChildren { + // section, + // children: lad_type + // .associated_functions + // .iter() + // .filter_map(|f| { + // let function = ladfile.functions.get(f)?; + // Some(SectionAndChildren { + // section: Section::FunctionDetail { function, ladfile }, + // children: vec![], + // }) + // }) + // .collect(), + // }) + // .collect(); - SectionAndChildren { - section: summary, - children, - } + // // now add a `functions` subsection before all types, for global functions + + // SectionAndChildren { + // section: summary, + // children: vec![ + // SectionAndChildren { + // section: Section::TypeSummary { ladfile }, + // children: types_children, + // }, + // SectionAndChildren { + // section: Section::FunctionSummary { ladfile }, + // children: vec![], + // }, + // ], + // } } pub(crate) struct SectionAndChildren<'a> { section: Section<'a>, @@ -111,43 +158,163 @@ pub(crate) enum Section<'a> { ladfile: &'a ladfile::LadFile, title: Option, }, + /// A link directory to all the types within the ladfile + TypeSummary { ladfile: &'a ladfile::LadFile }, + /// A link directory to all global functions within the ladfile + FunctionSummary { ladfile: &'a ladfile::LadFile }, + /// A link directory to all global instances within the ladfile + InstancesSummary { ladfile: &'a ladfile::LadFile }, TypeDetail { lad_type: &'a LadType, ladfile: &'a ladfile::LadFile, }, + FunctionDetail { + function: &'a LadFunction, + ladfile: &'a ladfile::LadFile, + }, } -impl Section<'_> { +/// Makes a filename safe to put in links +pub fn linkify_filename(name: impl Into) -> String { + name.into().to_lowercase().replace(" ", "_") +} + +impl<'a> Section<'a> { pub(crate) fn title(&self) -> String { match self { Section::Summary { title, .. } => { title.as_deref().unwrap_or("Bindings Summary").to_owned() } + Section::TypeSummary { .. } => "Types".to_owned(), + Section::FunctionSummary { .. } => "Functions".to_owned(), + Section::InstancesSummary { .. } => "Globals".to_owned(), Section::TypeDetail { lad_type: type_id, .. } => type_id.identifier.clone(), + Section::FunctionDetail { function, .. } => function.identifier.to_string(), } } pub(crate) fn file_name(&self) -> String { - self.title().to_lowercase().replace(" ", "_") + ".md" + linkify_filename(self.title()) + ".md" } - pub(crate) fn section_items(&self) -> Vec { + pub(crate) fn children(&self) -> Vec> { match self { Section::Summary { ladfile, .. } => { - let types = ladfile.types.values().collect::>(); - let instances = ladfile.globals.iter().collect::>(); vec![ - SectionItem::InstancesSummary { instances }, - SectionItem::TypesSummary { types }, + Section::TypeSummary { ladfile }, + Section::FunctionSummary { ladfile }, + Section::InstancesSummary { ladfile }, ] } - Section::TypeDetail { - lad_type: type_id, - ladfile, - } => { - let functions = type_id + Section::TypeSummary { ladfile } => ladfile + .types + .iter() + .map(|(_, lad_type)| Section::TypeDetail { lad_type, ladfile }) + .collect(), + + Section::FunctionSummary { ladfile } => { + let associated_functions = ladfile + .types + .iter() + .flat_map(|t| &t.1.associated_functions) + .collect::>(); + + let non_associated_functions = ladfile + .functions + .iter() + .filter_map(|f| (!associated_functions.contains(f.0)).then_some(f.1)); + + non_associated_functions + .map(|function| Section::FunctionDetail { function, ladfile }) + .collect() + } + Section::InstancesSummary { .. } => { + vec![] + } + Section::TypeDetail { lad_type, ladfile } => lad_type + .associated_functions + .iter() + .filter_map(|f| { + let function = ladfile.functions.get(f)?; + Some(Section::FunctionDetail { function, ladfile }) + }) + .collect(), + Section::FunctionDetail { .. } => vec![], + } + } + + pub(crate) fn section_items(&self) -> Vec { + match self { + Section::Summary { .. } => { + let mut builder = MarkdownBuilder::new(); + builder.heading(1, self.title()); + builder.heading(2, "Contents"); + builder.text("This is an automatically generated file, you'll find links to the contents below"); + builder.table(|builder| { + builder.headers(vec!["Section", "Contents"]); + builder.row(markdown_vec![ + Markdown::new_paragraph("Types").code(), + Markdown::Link { + text: "Describes all available binding types".into(), + url: format!("./{}/types.md", linkify_filename(self.title())), + anchor: false + } + ]); + builder.row(markdown_vec![ + Markdown::new_paragraph("Global Functions").code(), + Markdown::Link { + text: "Documents all the global functions present in the bindings" + .into(), + url: format!("./{}/functions.md", linkify_filename(self.title())), + anchor: false + } + ]); + builder.row(markdown_vec![ + Markdown::new_paragraph("Globals").code(), + Markdown::Link { + text: "Documents all global variables present in the bindings".into(), + url: format!("./{}/globals.md", linkify_filename(self.title())), + anchor: false + } + ]); + }); + vec![SectionItem::Markdown { + markdown: Box::new(builder), + }] + } + Section::InstancesSummary { ladfile } => { + let instances = ladfile.globals.iter().collect::>(); + vec![SectionItem::InstancesSummary { instances }] + } + Section::TypeSummary { ladfile } => { + let types = ladfile.types.values().collect::>(); + vec![SectionItem::TypesSummary { + types, + types_directory: linkify_filename(self.title()), + }] + } + Section::FunctionSummary { ladfile } => { + let associated_functions = ladfile + .types + .iter() + .flat_map(|t| &t.1.associated_functions) + .collect::>(); + + let non_associated_functions = ladfile + .functions + .iter() + .filter_map(|f| (!associated_functions.contains(f.0)).then_some(f.1)) + .collect(); + + vec![SectionItem::FunctionsSummary { + functions: non_associated_functions, + functions_directory: "functions".to_owned(), + }] + } + Section::TypeDetail { lad_type, ladfile } => { + let functions = lad_type .associated_functions .iter() .filter_map(|i| ladfile.functions.get(i)) @@ -155,12 +322,18 @@ impl Section<'_> { vec![ SectionItem::Layout { - layout: &type_id.layout, + layout: &lad_type.layout, + }, + SectionItem::Description { lad_type }, + SectionItem::FunctionsSummary { + functions, + functions_directory: linkify_filename(&lad_type.identifier), }, - SectionItem::Description { lad_type: type_id }, - SectionItem::FunctionsSummary { functions }, ] } + Section::FunctionDetail { function, ladfile } => { + vec![SectionItem::FunctionDetails { function, ladfile }] + } } } } @@ -175,8 +348,13 @@ impl IntoMarkdown for Section<'_> { } } +const NO_DOCS_STRING: &str = "No Documentation 🚧"; + /// Items which combine markdown elements to build a section pub enum SectionItem<'a> { + Markdown { + markdown: Box, + }, Layout { layout: &'a LadTypeLayout, }, @@ -185,9 +363,15 @@ pub enum SectionItem<'a> { }, FunctionsSummary { functions: Vec<&'a LadFunction>, + functions_directory: String, + }, + FunctionDetails { + function: &'a LadFunction, + ladfile: &'a ladfile::LadFile, }, TypesSummary { types: Vec<&'a LadType>, + types_directory: String, }, InstancesSummary { instances: Vec<(&'a Cow<'static, str>, &'a LadInstance)>, @@ -197,6 +381,7 @@ pub enum SectionItem<'a> { impl IntoMarkdown for SectionItem<'_> { fn to_markdown(&self, builder: &mut MarkdownBuilder) { match self { + SectionItem::Markdown { markdown } => markdown.to_markdown(builder), SectionItem::Layout { layout } => { // process the variants here let opaque = layout.for_each_variant( @@ -239,14 +424,18 @@ impl IntoMarkdown for SectionItem<'_> { SectionItem::Description { lad_type: description, } => { - builder.heading(2, "Description").quote( - description + builder.heading(2, "Description").quote(Markdown::Raw { + text: description .documentation .as_deref() - .unwrap_or("None available. 🚧"), - ); + .unwrap_or(NO_DOCS_STRING) + .to_owned(), + }); } - SectionItem::FunctionsSummary { functions } => { + SectionItem::FunctionsSummary { + functions, + functions_directory: functions_path, + } => { builder.heading(2, "Functions"); // make a table of functions as a quick reference, make them link to function details sub-sections @@ -272,28 +461,24 @@ impl IntoMarkdown for SectionItem<'_> { let second_col = function .documentation .as_deref() - .map(|doc| { - let doc = doc.trim(); - if doc.len() > 100 { - format!("{}...", &doc[..100]) - } else { - doc.to_owned() - } - }) - .unwrap_or_else(|| "No documentation available. 🚧".to_owned()); + .map(|doc| markdown_substring(doc, 100)) + .unwrap_or_else(|| NO_DOCS_STRING); builder.row(markdown_vec![ Markdown::new_paragraph(first_col).code(), Markdown::Link { - text: second_col, - url: function.identifier.to_string(), - anchor: true + text: second_col.to_owned(), + url: format!("./{}/{}.md", functions_path, function.identifier), + anchor: false } ]); } }); } - SectionItem::TypesSummary { types } => { + SectionItem::TypesSummary { + types, + types_directory, + } => { builder.heading(2, "Types"); // make a table of types as a quick reference, make them link to type details sub-sections @@ -314,14 +499,17 @@ impl IntoMarkdown for SectionItem<'_> { doc.to_owned() } }) - .unwrap_or_else(|| "No documentation available. 🚧".to_owned()); + .unwrap_or_else(|| NO_DOCS_STRING.to_owned()); builder.row(markdown_vec![ Markdown::new_paragraph(first_col).code(), Markdown::Link { text: second_col, - url: type_.identifier.to_string(), - anchor: true + url: format!( + "./{types_directory}/{}.md", + linkify_filename(&type_.identifier) + ), + anchor: false } ]); } @@ -343,6 +531,62 @@ impl IntoMarkdown for SectionItem<'_> { } }); } + SectionItem::FunctionDetails { function, ladfile } => { + // we don't escape this, this is already markdown + builder.quote(Markdown::Raw { + text: function + .documentation + .as_deref() + .unwrap_or(NO_DOCS_STRING) + .to_owned(), + }); + + builder.heading(4, "Arguments"); + builder.list( + false, + function + .arguments + .iter() + .enumerate() + .map(|(idx, arg)| lad_argument_to_list_elem(idx, arg, ladfile)) + .collect(), + ); + + builder.heading(4, "Returns"); + builder.list( + false, + vec![lad_argument_to_list_elem(0, &function.return_type, ladfile)], + ); + } } } } + +fn lad_argument_to_list_elem( + idx: usize, + arg: &LadArgument, + ladfile: &LadFile, +) -> impl IntoMarkdown { + let mut arg_visitor = MarkdownArgumentVisitor::new(ladfile); + arg_visitor.visit(&arg.kind); + let markdown = arg_visitor.build(); + + let arg_name = arg + .name + .as_ref() + .cloned() + .unwrap_or_else(|| Cow::Owned(format!("arg{}", idx))); + markdown_vec![ + Markdown::new_paragraph(arg_name).bold(), + Markdown::new_paragraph(":"), + Markdown::new_paragraph(markdown).code(), + Markdown::new_paragraph("-"), + Markdown::Raw { + text: arg + .documentation + .as_deref() + .unwrap_or(NO_DOCS_STRING) + .to_owned() + } + ] +} diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/book_integration_tests.rs b/crates/lad_backends/mdbook_lad_preprocessor/tests/book_integration_tests.rs index 9471fa6959..1a75ca6052 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/book_integration_tests.rs +++ b/crates/lad_backends/mdbook_lad_preprocessor/tests/book_integration_tests.rs @@ -27,14 +27,10 @@ fn get_books_dir() -> std::path::PathBuf { manifest_dir.join("tests").join("books") } -fn copy_ladfile_to_book_dir(book_dir: &std::path::Path, ladfile: &str) { - let ladfile_path = get_books_dir().join(ladfile); +fn copy_ladfile_to_book_dir(book_dir: &std::path::Path) { + let ladfile = ladfile::EXAMPLE_LADFILE; let book_ladfile_path = book_dir.join("src").join("test.lad.json"); - println!( - "Copying LAD file from {:?} to {:?}", - ladfile_path, book_ladfile_path - ); - std::fs::copy(ladfile_path, book_ladfile_path).expect("failed to copy LAD file"); + std::fs::write(book_ladfile_path, ladfile).expect("failed to copy LAD file"); } fn all_files_in_dir_recursive(dir: &std::path::Path) -> Vec { @@ -51,6 +47,11 @@ fn all_files_in_dir_recursive(dir: &std::path::Path) -> Vec files } +/// normalize line endings +fn normalize_file(file: String) -> String { + file.replace("\r\n", "\n") +} + #[test] fn test_on_example_ladfile() { // invoke mdbook build @@ -61,9 +62,7 @@ fn test_on_example_ladfile() { let books_dir = get_books_dir(); let book = "example_ladfile"; - let ladfile_path = "../../../../ladfile_builder/test_assets/test.lad.json"; - - copy_ladfile_to_book_dir(&books_dir.join(book), ladfile_path); + copy_ladfile_to_book_dir(&books_dir.join(book)); Command::new("mdbook") .env("RUST_LOG", "trace") @@ -90,6 +89,9 @@ fn test_on_example_ladfile() { let expected_content = std::fs::read_to_string(&expected_file).expect("failed to read file"); let book_content = std::fs::read_to_string(&book_file).expect("failed to read file"); - pretty_assertions::assert_eq!(expected_content, book_content); + pretty_assertions::assert_eq!( + normalize_file(expected_content), + normalize_file(book_content) + ); } } diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/enumtype.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/enumtype.md deleted file mode 100644 index ffe22b2e1f..0000000000 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/enumtype.md +++ /dev/null @@ -1,21 +0,0 @@ -# EnumType - -### Unit - -### Struct - -- **field** : usize - -### TupleStruct - -1. usize -2. String - -## Description - -> None available\. 🚧 - -## Functions - -| Function | Summary | -| --- | --- | diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/structtype.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/structtype.md deleted file mode 100644 index bbf6f07196..0000000000 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/structtype.md +++ /dev/null @@ -1,16 +0,0 @@ -# StructType - -### StructType - -- **field** : usize -- **field2** : usize - -## Description - -> I am a struct - -## Functions - -| Function | Summary | -| --- | --- | -| `hello_world(ref_, tuple, option_vec_ref_wrapper)` | [No documentation available\. 🚧](#helloworld) | \ No newline at end of file diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/tuplestructtype.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/tuplestructtype.md deleted file mode 100644 index a3b7079983..0000000000 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/tuplestructtype.md +++ /dev/null @@ -1,15 +0,0 @@ -# TupleStructType - -### TupleStructType - -1. usize -2. String - -## Description - -> I am a tuple test type - -## Functions - -| Function | Summary | -| --- | --- | diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/unittype.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/unittype.md deleted file mode 100644 index 2864cac1b5..0000000000 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad.md/unittype.md +++ /dev/null @@ -1,14 +0,0 @@ -# UnitType - -### UnitType - - - -## Description - -> I am a unit test type - -## Functions - -| Function | Summary | -| --- | --- | diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/functions.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/functions.md new file mode 100644 index 0000000000..547b97b243 --- /dev/null +++ b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/functions.md @@ -0,0 +1,7 @@ +# Functions + +## Functions + +| Function | Summary | +| --- | --- | +| `hello_world(arg1)` | [No Documentation 🚧](./functions/hello_world.md) | \ No newline at end of file diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/functions/hello_world.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/functions/hello_world.md new file mode 100644 index 0000000000..0d692dfbd3 --- /dev/null +++ b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/functions/hello_world.md @@ -0,0 +1,11 @@ +# hello\_world + +> No Documentation 🚧 + +#### Arguments + +- **arg1** : `usize` \- No Documentation 🚧 + +#### Returns + +- **arg0** : `usize` \- No Documentation 🚧 \ No newline at end of file diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/globals.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/globals.md new file mode 100644 index 0000000000..1a37b6fe81 --- /dev/null +++ b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/globals.md @@ -0,0 +1,8 @@ +# Globals + +## Globals + +| Instance | Type | +| --- | --- | +| `my_static_instance` | ladfile\_builder::test::StructType | +| `my_non_static_instance` | ladfile\_builder::test::UnitType | \ No newline at end of file diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/types.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/types.md new file mode 100644 index 0000000000..1592d341ed --- /dev/null +++ b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/lad/types.md @@ -0,0 +1,10 @@ +# Types + +## Types + +| Type | Summary | +| --- | --- | +| `EnumType` | [No Documentation 🚧](./types/enumtype.md) | +| `StructType` | [I am a struct](./types/structtype.md) | +| `TupleStructType` | [I am a tuple test type](./types/tuplestructtype.md) | +| `UnitType` | [I am a unit test type](./types/unittype.md) | \ No newline at end of file diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/test.lad.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/test.lad.md index 736b97b5db..b6b45e41d8 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/test.lad.md +++ b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/expected/test.lad.md @@ -1,17 +1,11 @@ -# Nested Lad +# LAD -## Globals +## Contents -| Instance | Type | -| --- | --- | -| `my_static_instance` | ladfile\_builder::test::StructType | -| `my_non_static_instance` | ladfile\_builder::test::UnitType | - -## Types +This is an automatically generated file, you'll find links to the contents below -| Type | Summary | +| Section | Contents | | --- | --- | -| `EnumType` | [No documentation available\. 🚧](#enumtype) | -| `StructType` | [I am a struct](#structtype) | -| `TupleStructType` | [I am a tuple test type](#tuplestructtype) | -| `UnitType` | [I am a unit test type](#unittype) | \ No newline at end of file +| `Types` | [Describes all available binding types](./lad/types.md) | +| `Global Functions` | [Documents all the global functions present in the bindings](./lad/functions.md) | +| `Globals` | [Documents all global variables present in the bindings](./lad/globals.md) | \ No newline at end of file diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/src/SUMMARY.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/src/SUMMARY.md index cb66e2c741..41b8c69c1e 100644 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/src/SUMMARY.md +++ b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/src/SUMMARY.md @@ -1,8 +1 @@ - -- [Some markdown](some_markdown.md) - - [LAD](test.lad.json) - -- [Some more markdown](some_markdown.md) - - - [Nested Lad](test.lad.json) \ No newline at end of file diff --git a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/src/some_markdown.md b/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/src/some_markdown.md deleted file mode 100644 index 44b705ed0e..0000000000 --- a/crates/lad_backends/mdbook_lad_preprocessor/tests/books/example_ladfile/src/some_markdown.md +++ /dev/null @@ -1,3 +0,0 @@ -# Blah - -blah \ No newline at end of file diff --git a/crates/ladfile/Cargo.toml b/crates/ladfile/Cargo.toml index 5da5077969..a8c43deb86 100644 --- a/crates/ladfile/Cargo.toml +++ b/crates/ladfile/Cargo.toml @@ -17,5 +17,11 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" indexmap = { version = "2.7", features = ["serde"] } + +[features] +default = ["visitor", "testfile"] +visitor = [] +testfile = [] + [lints] workspace = true diff --git a/crates/ladfile/readme.md b/crates/ladfile/readme.md index df8b3790fa..4a3288c9fb 100644 --- a/crates/ladfile/readme.md +++ b/crates/ladfile/readme.md @@ -9,4 +9,9 @@ A file format specifying the available exported: For a `bevy` game engine project. ## Example -See an example of a `LAD` file [here](./test_assets/test.lad.json) \ No newline at end of file +See an example of a `LAD` file [here](./test_assets/test.lad.json) + +## Features + +- `testfile` - Include the above testfile as a `ladfile::EXAMPLE_LADFILE` constant +- `visitor` - Provide traits for visiting parts of the `LAD` file. \ No newline at end of file diff --git a/crates/ladfile/src/lib.rs b/crates/ladfile/src/lib.rs index 5b89beb884..6c8964a322 100644 --- a/crates/ladfile/src/lib.rs +++ b/crates/ladfile/src/lib.rs @@ -1,4 +1,8 @@ //! Parsing definitions for the LAD (Language Agnostic Decleration) file format. +//! +//! The main ideals behind the format are: +//! - Centralization, we want to centralize as much of the "documentation" logic in the building of this format. For example, instead of letting each backend parse argument docstrings from the function docstring, we can do this here, and let the backends concentrate on pure generation. +//! - Rust centric, the format describes bindings from the Rust side, so we generate rust centric declarations. These can then freely be converted into whatever representaion necessary. use indexmap::IndexMap; use std::borrow::Cow; @@ -7,6 +11,10 @@ use std::borrow::Cow; /// Earlier versions are not guaranteed to be supported. pub const LAD_VERSION: &str = env!("CARGO_PKG_VERSION"); +/// The included example LAD file for testing purposes. +#[cfg(feature = "testfile")] +pub const EXAMPLE_LADFILE: &str = include_str!("../test_assets/test.lad.json"); + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] /// A Language Agnostic Declaration (LAD) file. pub struct LadFile { @@ -42,6 +50,25 @@ impl LadFile { description: None, } } + + /// Retrieves the best type identifier suitable for a type id. + pub fn get_type_identifier(&self, type_id: &LadTypeId) -> Cow<'static, str> { + if let Some(primitive) = self.primitives.get(type_id) { + return primitive.kind.lad_type_id().to_string().into(); + } + + self.types + .get(type_id) + .map(|t| t.identifier.clone().into()) + .unwrap_or_else(|| type_id.0.clone()) + } + + /// Retrieves the generics of a type id if it is a generic type. + pub fn get_type_generics(&self, type_id: &LadTypeId) -> Option<&[LadGeneric]> { + self.types + .get(type_id) + .and_then(|t| (!t.generics.is_empty()).then_some(t.generics.as_slice())) + } } impl Default for LadFile { @@ -94,7 +121,7 @@ pub struct LadFunction { #[serde(skip_serializing_if = "Vec::is_empty", default)] pub arguments: Vec, /// The return type of the function. - pub return_type: LadTypeId, + pub return_type: LadArgument, /// The documentation describing the function. #[serde(skip_serializing_if = "Option::is_none", default)] pub documentation: Option>, @@ -115,6 +142,11 @@ pub enum LadFunctionNamespace { pub struct LadArgument { /// The kind and type of argument pub kind: LadArgumentKind, + + /// The provided documentation for this argument. Normally derived from the function docstring. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub documentation: Option>, + /// The name of the argument #[serde(skip_serializing_if = "Option::is_none", default)] pub name: Option>, @@ -150,6 +182,99 @@ pub enum LadArgumentKind { Unknown(LadTypeId), } +/// A visitor pattern for running arbitrary logic on the hierarchy of arguments. +/// +/// Use cases are mostly to do with printing the arguments in a human readable format. +#[allow(unused_variables)] +#[cfg(feature = "visitor")] +pub trait ArgumentVisitor { + /// perform an action on a `LadTypeId`, by default noop + fn visit_lad_type_id(&mut self, type_id: &LadTypeId) {} + /// perform an action on a `LadBMSPrimitiveKind`, by default visits the type id of the primitive kind + fn visit_lad_bms_primitive_kind(&mut self, primitive_kind: &LadBMSPrimitiveKind) { + self.visit_lad_type_id(&primitive_kind.lad_type_id()); + } + + /// traverse a `Ref` wrapped argument, by default calls `visit` on the inner argument + fn walk_ref(&mut self, type_id: &LadTypeId) { + self.visit_lad_type_id(type_id); + } + + /// traverse a `Mut` wrapped argument, by default calls `visit` on the inner argument + fn walk_mut(&mut self, type_id: &LadTypeId) { + self.visit_lad_type_id(type_id); + } + + /// traverse a `Val` wrapped argument, by default calls `visit` on the inner argument + fn walk_val(&mut self, type_id: &LadTypeId) { + self.visit_lad_type_id(type_id); + } + + /// traverse an `Option` wrapped argument, by default calls `visit` on the inner argument + fn walk_option(&mut self, inner: &LadArgumentKind) { + self.visit(inner); + } + + /// traverse a `Vec` wrapped argument, by default calls `visit` on the inner argument + fn walk_vec(&mut self, inner: &LadArgumentKind) { + self.visit(inner); + } + + /// traverse a `HashMap` wrapped argument, by default calls `visit` on the key and value + fn walk_hash_map(&mut self, key: &LadArgumentKind, value: &LadArgumentKind) { + self.visit(key); + self.visit(value); + } + + /// traverse an `InteropResult` wrapped argument, by default calls `visit` on the inner argument + fn walk_interop_result(&mut self, inner: &LadArgumentKind) { + self.visit(inner); + } + + /// traverse a tuple of arguments, by default calls `visit` on each argument + fn walk_tuple(&mut self, inner: &[LadArgumentKind]) { + for arg in inner { + self.visit(arg); + } + } + + /// traverse an array of arguments, by default calls `visit` on the inner argument + fn walk_array(&mut self, inner: &LadArgumentKind, size: usize) { + self.visit(inner); + } + + /// traverse a primitive argument, by default calls `visit` on the primitive kind + fn walk_primitive(&mut self, primitive_kind: &LadBMSPrimitiveKind) { + self.visit_lad_bms_primitive_kind(primitive_kind); + } + + /// traverse an unknown argument, by default calls `visit` on the type id + fn walk_unknown(&mut self, type_id: &LadTypeId) { + self.visit_lad_type_id(type_id); + } + + /// Visit an argument kind, by default calls the appropriate walk method on each enum variant. + /// + /// Each walk variant will walk over nested kinds, and visit the leaf types. + /// + /// If you want to do something with the parent types, you WILL have to override each individual walk method. + fn visit(&mut self, kind: &LadArgumentKind) { + match kind { + LadArgumentKind::Ref(type_id) => self.walk_ref(type_id), + LadArgumentKind::Mut(type_id) => self.walk_mut(type_id), + LadArgumentKind::Val(type_id) => self.walk_val(type_id), + LadArgumentKind::Option(inner) => self.walk_option(inner), + LadArgumentKind::Vec(inner) => self.walk_vec(inner), + LadArgumentKind::HashMap(key, value) => self.walk_hash_map(key, value), + LadArgumentKind::InteropResult(inner) => self.walk_interop_result(inner), + LadArgumentKind::Tuple(inner) => self.walk_tuple(inner), + LadArgumentKind::Array(inner, size) => self.walk_array(inner, *size), + LadArgumentKind::Primitive(primitive_kind) => self.walk_primitive(primitive_kind), + LadArgumentKind::Unknown(type_id) => self.walk_unknown(type_id), + } + } +} + #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] /// A BMS primitive definition pub struct LadBMSPrimitiveType { @@ -162,7 +287,7 @@ pub struct LadBMSPrimitiveType { /// A primitive type kind in the LAD file format. /// /// The docstrings on variants corresponding to Reflect types, are used to generate documentation for these primitives. -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "camelCase")] #[allow(missing_docs)] pub enum LadBMSPrimitiveKind { diff --git a/crates/ladfile_builder/test_assets/test.lad.json b/crates/ladfile/test_assets/test.lad.json similarity index 92% rename from crates/ladfile_builder/test_assets/test.lad.json rename to crates/ladfile/test_assets/test.lad.json index 990d6efce7..27a318c148 100644 --- a/crates/ladfile_builder/test_assets/test.lad.json +++ b/crates/ladfile/test_assets/test.lad.json @@ -114,7 +114,11 @@ "name": "arg1" } ], - "return_type": "usize" + "return_type": { + "kind": { + "primitive": "usize" + } + } }, "ladfile_builder::test::StructType::hello_world": { "namespace": "ladfile_builder::test::StructType", @@ -124,6 +128,7 @@ "kind": { "primitive": "reflectReference" }, + "documentation": "I am some docs for argument 1", "name": "ref_" }, { @@ -137,6 +142,7 @@ } ] }, + "documentation": "I am some docs for argument 2", "name": "tuple" }, { @@ -147,10 +153,17 @@ } } }, + "documentation": "I am some docs for argument 3", "name": "option_vec_ref_wrapper" } ], - "return_type": "usize" + "return_type": { + "kind": { + "primitive": "usize" + }, + "documentation": "I am some docs for the return type, I provide a name for the return value too", + "name": "return" + } } }, "primitives": { diff --git a/crates/ladfile_builder/Cargo.toml b/crates/ladfile_builder/Cargo.toml index 6ebcaef419..5d77b2ad0b 100644 --- a/crates/ladfile_builder/Cargo.toml +++ b/crates/ladfile_builder/Cargo.toml @@ -17,6 +17,7 @@ bevy_mod_scripting_core = { workspace = true } # I don't think bevy has a top level feature for this :C bevy_reflect = { version = "0.15.2", features = ["documentation"] } ladfile = { version = "0.2.0", path = "../ladfile" } +regex = "1.11" [dev-dependencies] pretty_assertions = "1.4" diff --git a/crates/ladfile_builder/src/lib.rs b/crates/ladfile_builder/src/lib.rs index 150973d4f6..39c4f3f18b 100644 --- a/crates/ladfile_builder/src/lib.rs +++ b/crates/ladfile_builder/src/lib.rs @@ -16,7 +16,14 @@ use bevy_reflect::{ NamedField, TypeInfo, TypeRegistry, Typed, UnnamedField, }; use ladfile::*; -use std::{any::TypeId, borrow::Cow, collections::HashMap, ffi::OsString, path::PathBuf}; +use std::{ + any::TypeId, + borrow::Cow, + cmp::{max, min}, + collections::HashMap, + ffi::OsString, + path::PathBuf, +}; /// We can assume that the types here will be either primitives /// or reflect types, as the rest will be covered by typed wrappers @@ -186,7 +193,25 @@ impl<'t> LadFileBuilder<'t> { /// Add a function definition to the LAD file. /// Will overwrite any existing function definitions with the same function id. + /// + /// Parses argument and return specific docstrings as per: https://github.com/rust-lang/rust/issues/57525 + /// + /// i.e. looks for blocks like: + /// ```rust,ignore + /// /// Arguments: + /// /// * `arg_name`: docstring1 + /// /// * `arg_name2`: docstring2 + /// /// + /// /// Returns: + /// /// * `return_name`: return docstring + /// ``` + /// + /// And then removes them from the original block, instead putting it in each argument / return docstring pub fn add_function_info(&mut self, function_info: FunctionInfo) -> &mut Self { + let default_docstring = Cow::Owned("".into()); + let (main_docstring, arg_docstrings, return_docstring) = + Self::split_docstring(function_info.docs.as_ref().unwrap_or(&default_docstring)); + let function_id = self.lad_function_id_from_info(&function_info); let lad_function = LadFunction { identifier: function_info.name, @@ -202,12 +227,28 @@ impl<'t> LadFileBuilder<'t> { }; LadArgument { kind, + documentation: arg_docstrings.iter().find_map(|(name, doc)| { + (Some(name.as_str()) == arg.name.as_deref()) + .then_some(Cow::Owned(doc.clone())) + }), name: arg.name, } }) .collect(), - return_type: self.lad_id_from_type_id(function_info.return_info.type_id), - documentation: function_info.docs, + return_type: LadArgument { + name: return_docstring.as_ref().cloned().map(|(n, _)| n.into()), + documentation: return_docstring.map(|(_, v)| v.into()), + kind: function_info + .return_info + .type_info + .map(|info| self.lad_argument_type_from_through_type(&info)) + .unwrap_or_else(|| { + LadArgumentKind::Unknown( + self.lad_id_from_type_id(function_info.return_info.type_id), + ) + }), + }, + documentation: (!main_docstring.is_empty()).then_some(main_docstring.into()), namespace: match function_info.namespace { Namespace::Global => LadFunctionNamespace::Global, Namespace::OnType(type_id) => { @@ -249,6 +290,125 @@ impl<'t> LadFileBuilder<'t> { file } + /// Checks if a line is one of: + /// - `# key:` + /// - `key:` + /// - `key` + /// - `## key` + /// + /// Or similar patterns + fn is_docstring_delimeter(key: &str, line: &str) -> bool { + line.trim() + .trim_start_matches("#") + .trim_end_matches(":") + .trim() + .eq_ignore_ascii_case(key) + } + + /// Parses lines of the pattern: + /// * `arg` : val + /// + /// returning (arg,val) without markup + fn parse_arg_docstring(line: &str) -> Option<(&str, &str)> { + let regex = + regex::Regex::new(r#"\s*\*\s*`(?[^`]+)`\s*[:-]\s*(?.+[^\s]).*$"#).ok()?; + let captures = regex.captures(line)?; + let arg = captures.name("arg")?; + let val = captures.name("val")?; + + Some((arg.as_str(), val.as_str())) + } + + /// Splits the docstring, into the following: + /// - The main docstring + /// - The argument docstrings + /// - The return docstring + /// + /// While removing any prefixes + fn split_docstring( + docstring: &str, + ) -> (String, Vec<(String, String)>, Option<(String, String)>) { + // find a line containing only `Arguments:` ignoring spaces and markdown headings + let lines = docstring.lines().collect::>(); + + // this must exist for us to parse any of the areas + let argument_line_idx = match lines + .iter() + .enumerate() + .find_map(|(idx, l)| Self::is_docstring_delimeter("arguments", l).then_some(idx)) + { + Some(a) => a, + None => return (docstring.to_owned(), vec![], None), + }; + + // this can, not exist, if arguments area does + let return_line_idx = lines.iter().enumerate().find_map(|(idx, l)| { + (Self::is_docstring_delimeter("returns", l) + || Self::is_docstring_delimeter("return", l)) + .then_some(idx) + }); + + let return_area_idx = return_line_idx.unwrap_or(usize::MAX); + let return_area_first = argument_line_idx > return_area_idx; + let argument_range = match return_area_first { + true => argument_line_idx..lines.len(), + false => argument_line_idx..return_area_idx, + }; + let return_range = match return_area_first { + true => return_area_idx..argument_line_idx, + false => return_area_idx..lines.len(), + }; + let non_main_area = + min(return_area_idx, argument_line_idx)..max(return_area_idx, argument_line_idx); + + let parsed_lines = lines + .iter() + .enumerate() + .map(|(i, l)| { + match Self::parse_arg_docstring(l) { + Some(parsed) => { + // figure out if it's in the argument, return or neither of the areas + // if return area doesn't exist assign everything to arguments + let in_argument_range = argument_range.contains(&i); + let in_return_range = return_range.contains(&i); + (l, Some((in_argument_range, in_return_range, parsed))) + } + None => (l, None), + } + }) + .collect::>(); + + // collect all argument docstrings, and the first return docstring, removing those lines from the docstring (and the argument/return headers) + // any other ones leave alone + let main_docstring = parsed_lines + .iter() + .enumerate() + .filter_map(|(i, (l, parsed))| { + ((!non_main_area.contains(&i) || !l.trim().is_empty()) + && (i != return_area_idx && i != argument_line_idx) + && (parsed.is_none() || parsed.is_some_and(|(a, b, _)| !a && !b))) + .then_some((**l).to_owned()) + }) + .collect::>(); + + let arg_docstrings = parsed_lines + .iter() + .filter_map(|(_l, parsed)| { + parsed.and_then(|(is_arg, is_return, (a, b))| { + (is_arg && !is_return).then_some((a.to_owned(), b.to_owned())) + }) + }) + .collect(); + + let return_docstring = parsed_lines.iter().find_map(|(_l, parsed)| { + parsed.and_then(|(is_arg, is_return, (a, b))| { + (!is_arg && is_return).then_some((a.to_owned(), b.to_owned())) + }) + }); + + (main_docstring.join("\n"), arg_docstrings, return_docstring) + } + fn variant_identifier_for_non_enum(type_info: &TypeInfo) -> Cow<'static, str> { type_info .type_path_table() @@ -462,6 +622,187 @@ mod test { assert_eq!(deserialized.version, ladfile::LAD_VERSION); } + #[test] + fn parse_docstrings_is_resistant_to_whitespace() { + pretty_assertions::assert_eq!( + LadFileBuilder::parse_arg_docstring("* `arg` : doc"), + Some(("arg", "doc")) + ); + pretty_assertions::assert_eq!( + LadFileBuilder::parse_arg_docstring(" * `arg` - doc"), + Some(("arg", "doc")) + ); + pretty_assertions::assert_eq!( + LadFileBuilder::parse_arg_docstring(" * `arg` : doc "), + Some(("arg", "doc")) + ); + } + + #[test] + fn docstring_delimeter_detection_is_flexible() { + assert!(LadFileBuilder::is_docstring_delimeter( + "arguments", + "arguments" + )); + assert!(LadFileBuilder::is_docstring_delimeter( + "arguments", + "Arguments:" + )); + assert!(LadFileBuilder::is_docstring_delimeter( + "arguments", + "## Arguments" + )); + assert!(LadFileBuilder::is_docstring_delimeter( + "arguments", + "## Arguments:" + )); + assert!(LadFileBuilder::is_docstring_delimeter( + "arguments", + "Arguments" + )); + } + + /// Helper function to assert that splitting the docstring produces the expected output. + fn assert_docstring_split( + input: &str, + expected_main: &str, + expected_args: &[(&str, &str)], + expected_return: Option<(&str, &str)>, + test_name: &str, + ) { + let (main, args, ret) = LadFileBuilder::split_docstring(input); + + pretty_assertions::assert_eq!( + main, + expected_main, + "main docstring was incorrect - {}", + test_name + ); + + let expected_args: Vec<(String, String)> = expected_args + .iter() + .map(|(a, b)| (a.to_string(), b.to_string())) + .collect(); + pretty_assertions::assert_eq!( + args, + expected_args, + "argument docstring was incorrect - {}", + test_name + ); + + let expected_ret = expected_return.map(|(a, b)| (a.to_string(), b.to_string())); + pretty_assertions::assert_eq!( + ret, + expected_ret, + "return docstring was incorrect - {}", + test_name + ); + } + + #[test] + fn docstrings_parse_correctly_from_various_formats() { + assert_docstring_split( + r#" + ## Hello + Arguments: + * `arg1` - some docs + * `arg2` : some more docs + # Returns + * `return` : return docs + "# + .trim(), + "## Hello", + &[("arg1", "some docs"), ("arg2", "some more docs")], + Some(("return", "return docs")), + "normal docstring", + ); + assert_docstring_split( + r#" + Arguments: + * `arg1` - some docs + * `arg2` : some more docs + Returns + * `return` : return docs + "# + .trim(), + "", + &[("arg1", "some docs"), ("arg2", "some more docs")], + Some(("return", "return docs")), + "empty main docstring", + ); + assert_docstring_split( + r#" + Arguments: + * `arg1` - some docs + * `arg2` : some more docs + "# + .trim(), + "", + &[("arg1", "some docs"), ("arg2", "some more docs")], + None, + "no return docstring", + ); + assert_docstring_split( + r#" + Returns + * `return` : return docs + "# + .trim(), + r#" + Returns + * `return` : return docs + "# + .trim(), + &[], + None, + "no argument docstring", + ); + assert_docstring_split( + r#" + ## Hello + "# + .trim(), + "## Hello", + &[], + None, + "no argument or return docstring", + ); + // return first + assert_docstring_split( + r#" + Returns + * `return` : return docs + Arguments: + * `arg1` - some docs + * `arg2` : some more docs + "# + .trim(), + "", + &[("arg1", "some docs"), ("arg2", "some more docs")], + Some(("return", "return docs")), + "return first", + ); + // whitespace in between + assert_docstring_split( + r#" + ## Hello + + + Arguments: + * `arg1` - some docs + * `arg2` : some more docs + + Returns + * `return` : return docs + "# + .trim(), + "## Hello\n\n", + &[("arg1", "some docs"), ("arg2", "some more docs")], + Some(("return", "return docs")), + "whitespace in between", + ); + } + #[test] fn test_serializes_as_expected() { let mut type_registry = TypeRegistry::default(); @@ -510,7 +851,20 @@ mod test { |_: ReflectReference, _: (usize, String), _: Option>>| 2usize; let function_with_complex_args_info = function_with_complex_args .get_function_info("hello_world".into(), StructType::::into_namespace()) - .with_arg_names(&["ref_", "tuple", "option_vec_ref_wrapper"]); + .with_arg_names(&["ref_", "tuple", "option_vec_ref_wrapper"]) + .with_docs( + "Arguments: ".to_owned() + + "\n" + + " * `ref_`: I am some docs for argument 1" + + "\n" + + " * `tuple`: I am some docs for argument 2" + + "\n" + + " * `option_vec_ref_wrapper`: I am some docs for argument 3" + + "\n" + + "Returns: " + + "\n" + + " * `return`: I am some docs for the return type, I provide a name for the return value too", + ); let global_function = |_: usize| 2usize; let global_function_info = global_function @@ -539,14 +893,17 @@ mod test { if BLESS_TEST_FILE { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let path_to_test_assets = std::path::Path::new(&manifest_dir).join("test_assets"); + let path_to_test_assets = std::path::Path::new(&manifest_dir) + .join("..") + .join("ladfile") + .join("test_assets"); println!("Blessing test file at {:?}", path_to_test_assets); std::fs::write(path_to_test_assets.join("test.lad.json"), &serialized).unwrap(); return; } - let mut expected = include_str!("../test_assets/test.lad.json").to_owned(); + let mut expected = ladfile::EXAMPLE_LADFILE.to_string(); normalize_file(&mut expected); pretty_assertions::assert_eq!(serialized.trim(), expected.trim(),); @@ -554,8 +911,8 @@ mod test { #[test] fn test_asset_deserializes_correctly() { - let asset = include_str!("../test_assets/test.lad.json"); - let deserialized = parse_lad_file(asset).unwrap(); + let asset = ladfile::EXAMPLE_LADFILE.to_string(); + let deserialized = parse_lad_file(&asset).unwrap(); assert_eq!(deserialized.version, "{{version}}"); } }