diff --git a/extensions/scarb-doc/src/compilation.rs b/extensions/scarb-doc/src/compilation.rs index 71c2aa9b0..6560da255 100644 --- a/extensions/scarb-doc/src/compilation.rs +++ b/extensions/scarb-doc/src/compilation.rs @@ -53,6 +53,7 @@ fn package_compilation_unit( }) .expect("failed to find compilation unit for package") } + fn get_corelib( compilation_unit_metadata: &CompilationUnitMetadata, ) -> &CompilationUnitComponentMetadata { diff --git a/extensions/scarb-doc/src/main.rs b/extensions/scarb-doc/src/main.rs index 2c874077b..e3959ca56 100644 --- a/extensions/scarb-doc/src/main.rs +++ b/extensions/scarb-doc/src/main.rs @@ -12,7 +12,10 @@ use cairo_lang_starknet::starknet_plugin_suite; use compilation::get_project_config; use types::Crate; +use std::path::PathBuf; + mod compilation; +mod render; mod types; #[derive(Parser, Debug)] @@ -20,6 +23,10 @@ mod types; struct Args { #[command(flatten)] packages_filter: PackagesFilter, + + /// Generate the documentation. + #[arg(long)] + save: bool, } macro_rules! print_names { @@ -53,6 +60,12 @@ fn main() -> Result<()> { print_module(&crate_.root_module); println!("{crate_:?}"); + if args.save { + let path = PathBuf::from(crate_.root_module.name.as_str()); + let doc_path = PathBuf::from("doc").join(path); + crate_.root_module.save_to_file_recursive(&doc_path)?; + } + Ok(()) } diff --git a/extensions/scarb-doc/src/render.rs b/extensions/scarb-doc/src/render.rs new file mode 100644 index 000000000..283d32e18 --- /dev/null +++ b/extensions/scarb-doc/src/render.rs @@ -0,0 +1,236 @@ +use anyhow::Result; + +use crate::types::{Module, Trait, TraitConstant, TraitFunction, TraitType}; + +use std::{fmt::Write, path::Path}; + +pub trait Markdown { + fn md_ref(&self) -> String; + fn generate_markdown(&self) -> Result; + fn generate_markdown_brief(&self) -> String; + #[allow(dead_code)] + fn generate_markdown_list_item(&self) -> String { + format!("- {}\n", self.md_ref()) + } +} + +pub trait ToC { + fn generate_toc(&self) -> String; +} + +impl Markdown for Module { + fn md_ref(&self) -> String { + format!( + "[{}]({}.md)", + self.name, + self.full_path.to_lowercase().replace(' ', "-") + ) + } + + fn generate_markdown(&self) -> Result { + let mut markdown = String::new(); + + writeln!(&mut markdown, "# Module {}\n", self.full_path_ref())?; + + writeln!(&mut markdown, "{}", self.generate_toc())?; + + writeln!(&mut markdown, "## Submodules\n").unwrap(); + for submodule in &self.submodules { + writeln!(&mut markdown, "\n{}\n", submodule.generate_markdown_brief())?; + } + + writeln!(&mut markdown, "## Traits\n").unwrap(); + for trait_ in &self.traits { + writeln!(&mut markdown, "\n{}\n", trait_.generate_markdown()?)?; + } + + Ok(markdown) + } + + fn generate_markdown_brief(&self) -> String { + format!( + "### {}\n{}", + self.md_ref(), + self.doc.as_ref().unwrap_or(&String::new()) + ) + } + + fn generate_markdown_list_item(&self) -> String { + format!("- {}\n", self.md_ref()) + } +} + +macro_rules! add_section { + ($markdown:expr, $title:expr, $condition:expr) => { + if $condition { + writeln!( + $markdown, + "- [{}](#{})", + $title, + $title.to_lowercase().replace(' ', "-") + ) + .unwrap(); + } + }; +} + +impl ToC for Module { + fn generate_toc(&self) -> String { + let mut markdown = String::new(); + + add_section!(&mut markdown, "Submodules", !self.submodules.is_empty()); + add_section!(&mut markdown, "Structs", !self.structs.is_empty()); + add_section!(&mut markdown, "Enums", !self.enums.is_empty()); + add_section!(&mut markdown, "Functions", !self.free_functions.is_empty()); + add_section!(&mut markdown, "Constants", !self.constants.is_empty()); + add_section!(&mut markdown, "Type Aliases", !self.type_aliases.is_empty()); + add_section!(&mut markdown, "Impl Aliases", !self.impl_aliases.is_empty()); + add_section!(&mut markdown, "Traits", !self.traits.is_empty()); + add_section!(&mut markdown, "Impls", !self.impls.is_empty()); + add_section!(&mut markdown, "Extern Types", !self.extern_types.is_empty()); + add_section!( + &mut markdown, + "Extern Functions", + !self.extern_functions.is_empty() + ); + + markdown + } +} + +impl Module { + pub fn full_path_ref(&self) -> String { + let parts: Vec<&str> = self.full_path.split("::").collect(); + + let mut curr_path = String::new(); + parts + .iter() + .enumerate() + .map(|(index, &part)| { + if index > 0 { + curr_path.push_str("::"); + } + curr_path.push_str(part); + + if index != parts.len() - 1 { + format!("[{}]({}.md)", part, curr_path) + } else { + part.to_string() + } + }) + .collect::>() + .join("::") + } + + pub fn save_to_file_recursive(&self, directory: &Path) -> Result<()> { + let markdown = self.generate_markdown()?; + + std::fs::create_dir_all(directory)?; + std::fs::write(directory.join(format!("{}.md", self.full_path)), markdown)?; + + for submodule in &self.submodules { + submodule.save_to_file_recursive(directory)?; + } + + Ok(()) + } +} + +impl Markdown for Trait { + fn md_ref(&self) -> String { + format!( + "[{}]({}.md#{})", + self.item_data.name, + self.item_data.full_path.to_lowercase(), + self.item_data.name + ) + } + + fn generate_markdown(&self) -> Result { + let mut markdown = String::new(); + + writeln!(&mut markdown, "### {}\n", self.item_data.name)?; + writeln!( + &mut markdown, + "{}", + self.item_data.doc.as_ref().unwrap_or(&String::new()) + )?; + + writeln!( + &mut markdown, + "```rust\n{}\n```\n", + self.item_data.signature + )?; + + if !self.trait_constants.is_empty() { + writeln!(&mut markdown, "#### Trait Constants\n",)?; + for trait_const in &self.trait_constants { + writeln!(&mut markdown, "{}", trait_const.generate_markdown()?)?; + } + } + + if !self.trait_types.is_empty() { + writeln!(&mut markdown, "#### Trait Types\n",)?; + for trait_type in &self.trait_types { + writeln!(&mut markdown, "{}", trait_type.generate_markdown()?)?; + } + } + + if !self.trait_functions.is_empty() { + writeln!(&mut markdown, "#### Trait Functions\n",)?; + for trait_func in &self.trait_functions { + writeln!(&mut markdown, "{}", trait_func.generate_markdown()?)?; + } + } + + Ok(markdown) + } + + fn generate_markdown_brief(&self) -> String { + format!( + "### {}\n{}", + self.md_ref(), + self.item_data.doc.as_ref().unwrap_or(&String::new()) + ) + } +} + +macro_rules! impl_md { + ($t:ty) => { + impl Markdown for $t { + fn md_ref(&self) -> String { + format!( + "[{}]({}.md#{})", + self.item_data.name, + self.item_data.full_path.to_lowercase(), + self.item_data.name + ) + } + + fn generate_markdown(&self) -> Result { + let mut markdown = String::new(); + + writeln!(&mut markdown, "{}", self.generate_markdown_brief())?; + writeln!( + &mut markdown, + "```rust\n{}\n```\n", + self.item_data.text_without_trivia + )?; + + Ok(markdown) + } + + fn generate_markdown_brief(&self) -> String { + format!( + "##### {}\n{}", + self.item_data.name, + self.item_data.doc.as_ref().unwrap_or(&String::new()) + ) + } + } + }; +} + +impl_md!(TraitConstant); +impl_md!(TraitType); +impl_md!(TraitFunction); diff --git a/extensions/scarb-doc/src/types.rs b/extensions/scarb-doc/src/types.rs index 3397b5e4f..7d6967491 100644 --- a/extensions/scarb-doc/src/types.rs +++ b/extensions/scarb-doc/src/types.rs @@ -7,8 +7,9 @@ use cairo_lang_defs::diagnostic_utils::StableLocation; use cairo_lang_defs::ids::{ ConstantId, EnumId, ExternFunctionId, ExternTypeId, FreeFunctionId, ImplAliasId, ImplConstantDefId, ImplDefId, ImplFunctionId, ImplItemId, ImplTypeDefId, LookupItemId, - MemberId, ModuleId, ModuleItemId, ModuleTypeAliasId, StructId, TopLevelLanguageElementId, - TraitConstantId, TraitFunctionId, TraitId, TraitItemId, TraitTypeId, UseId, VariantId, + MemberId, ModuleId, ModuleItemId, ModuleTypeAliasId, NamedLanguageElementId, StructId, + TopLevelLanguageElementId, TraitConstantId, TraitFunctionId, TraitId, TraitItemId, TraitTypeId, + UseId, VariantId, }; use cairo_lang_filesystem::ids::CrateId; use cairo_lang_semantic::db::SemanticGroup; @@ -36,6 +37,8 @@ impl Crate { pub struct Module { pub module_id: ModuleId, pub full_path: String, + pub name: String, + pub doc: Option, pub submodules: Vec, pub constants: Vec, @@ -125,9 +128,19 @@ impl Module { .map(|(id, _)| Self::new(db, ModuleId::Submodule(*id))) .collect(); + let (name, doc) = match module_id { + ModuleId::CrateRoot(id) => (id.name(db).into(), None), + ModuleId::Submodule(id) => ( + id.name(db).into(), + db.get_item_documentation(LookupItemId::ModuleItem(ModuleItemId::Submodule(id))), + ), + }; + Self { module_id, full_path: module_id.full_path(db), + name, + doc, submodules, constants, uses,