diff --git a/forc-plugins/forc-doc/src/doc.rs b/forc-plugins/forc-doc/src/doc.rs index 0972e8459a..910fd8a50d 100644 --- a/forc-plugins/forc-doc/src/doc.rs +++ b/forc-plugins/forc-doc/src/doc.rs @@ -1,6 +1,6 @@ use crate::{ descriptor::Descriptor, - render::{split_at_markdown_header, DocLink, ItemBody, ItemHeader, Renderable}, + render::{split_at_markdown_header, DocLink, DocStrings, ItemBody, ItemHeader, Renderable}, }; use anyhow::Result; use horrorshow::{box_html, RenderBox}; @@ -50,35 +50,7 @@ impl Document { } } fn preview_opt(&self) -> Option { - const MAX_PREVIEW_CHARS: usize = 100; - const CLOSING_PARAGRAPH_TAG: &str = "

"; - - self.raw_attributes.as_ref().map(|description| { - let preview = split_at_markdown_header(description); - if preview.chars().count() > MAX_PREVIEW_CHARS - && preview.contains(CLOSING_PARAGRAPH_TAG) - { - match preview.find(CLOSING_PARAGRAPH_TAG) { - Some(index) => { - // We add 1 here to get the index of the char after the closing tag. - // This ensures we retain the closing tag and don't break the html. - let (preview, _) = - preview.split_at(index + CLOSING_PARAGRAPH_TAG.len() + 1); - if preview.chars().count() > MAX_PREVIEW_CHARS && preview.contains('\n') { - match preview.find('\n') { - Some(index) => preview.split_at(index).0.to_string(), - None => unreachable!("Previous logic prevents this panic"), - } - } else { - preview.to_string() - } - } - None => unreachable!("Previous logic prevents this panic"), - } - } else { - preview.to_string() - } - }) + create_preview(self.raw_attributes.clone()) } /// Gather [Documentation] from the [TyProgram]. pub(crate) fn from_ty_program( @@ -95,7 +67,7 @@ impl Document { let desc = Descriptor::from_typed_decl( decl_engine, decl, - ModuleInfo::from_vec(vec![project_name.to_owned()]), + ModuleInfo::from_ty_module(vec![project_name.to_owned()], None), document_private_items, )?; @@ -108,7 +80,10 @@ impl Document { if !no_deps && !typed_program.root.submodules.is_empty() { // this is the same process as before but for dependencies for (_, ref typed_submodule) in &typed_program.root.submodules { - let module_prefix = ModuleInfo::from_vec(vec![project_name.to_owned()]); + let attributes = (!typed_submodule.module.attributes.is_empty()) + .then(|| typed_submodule.module.attributes.to_html_string()); + let module_prefix = + ModuleInfo::from_ty_module(vec![project_name.to_owned()], attributes); Document::from_ty_submodule( decl_engine, typed_submodule, @@ -130,7 +105,7 @@ impl Document { ) -> Result<()> { let mut new_submodule_prefix = module_prefix.to_owned(); new_submodule_prefix - .0 + .module_prefixes .push(typed_submodule.library_name.as_str().to_owned()); for ast_node in &typed_submodule.module.all_nodes { if let TyAstNodeContent::Declaration(ref decl) = ast_node.content { @@ -173,13 +148,16 @@ impl Renderable for Document { pub(crate) type ModulePrefix = String; #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub(crate) struct ModuleInfo(pub(crate) Vec); +pub(crate) struct ModuleInfo { + pub(crate) module_prefixes: Vec, + pub(crate) attributes: Option, +} impl ModuleInfo { /// The current module. /// /// Panics if there are no modules. pub(crate) fn location(&self) -> &str { - self.0 + self.module_prefixes .last() .expect("Expected Some module location, found None") } @@ -187,7 +165,7 @@ impl ModuleInfo { /// /// Panics if the project root is missing. pub(crate) fn project_name(&self) -> &str { - self.0 + self.module_prefixes .first() .expect("Expected root module, project root missing") } @@ -197,7 +175,7 @@ impl ModuleInfo { pub(crate) fn parent(&self) -> Option<&String> { match self.has_parent() { true => { - let mut iter = self.0.iter(); + let mut iter = self.module_prefixes.iter(); iter.next_back(); iter.next_back() } @@ -226,7 +204,7 @@ impl ModuleInfo { /// /// Example: `module::submodule` fn to_path_literal_prefix(&self, location: &str) -> String { - let mut iter = self.0.iter(); + let mut iter = self.module_prefixes.iter(); for prefix in iter.by_ref() { if prefix == location { break; @@ -239,7 +217,7 @@ impl ModuleInfo { /// /// This is only used for full path syntax, e.g `module/submodule/file_name.html`. pub(crate) fn to_file_path_string(&self, file_name: &str, location: &str) -> Result { - let mut iter = self.0.iter(); + let mut iter = self.module_prefixes.iter(); for prefix in iter.by_ref() { if prefix == location { break; @@ -265,12 +243,47 @@ impl ModuleInfo { } /// The depth of a module as `usize`. pub(crate) fn depth(&self) -> usize { - self.0.len() + self.module_prefixes.len() } /// Create a new [ModuleInfo] from a vec. - pub(crate) fn from_vec(vec: Vec) -> Self { - Self(vec) + pub(crate) fn from_ty_module(module_prefixes: Vec, attributes: Option) -> Self { + Self { + module_prefixes, + attributes, + } } + pub(crate) fn preview_opt(&self) -> Option { + create_preview(self.attributes.clone()) + } +} + +/// Create a docstring preview from raw html attributes. +/// +/// Returns `None` if there are no attributes. +fn create_preview(raw_attributes: Option) -> Option { + const MAX_PREVIEW_CHARS: usize = 100; + const CLOSING_PARAGRAPH_TAG: &str = "

"; + + raw_attributes.as_ref().map(|description| { + let preview = split_at_markdown_header(description); + if preview.chars().count() > MAX_PREVIEW_CHARS && preview.contains(CLOSING_PARAGRAPH_TAG) { + let closing_tag_index = preview + .find(CLOSING_PARAGRAPH_TAG) + .expect("closing tag out of range"); + // We add 1 here to get the index of the char after the closing tag. + // This ensures we retain the closing tag and don't break the html. + let (preview, _) = + preview.split_at(closing_tag_index + CLOSING_PARAGRAPH_TAG.len() + 1); + if preview.chars().count() > MAX_PREVIEW_CHARS && preview.contains('\n') { + let newline_index = preview.find('\n').expect("new line char out of range"); + preview.split_at(newline_index).0.to_string() + } else { + preview.to_string() + } + } else { + preview.to_string() + } + }) } #[cfg(test)] @@ -283,12 +296,12 @@ mod tests { let module = String::from("module_name"); let mut module_vec = vec![project.clone(), module]; - let module_info = ModuleInfo::from_vec(module_vec.clone()); + let module_info = ModuleInfo::from_ty_module(module_vec.clone(), None); let project_opt = module_info.parent(); assert_eq!(Some(&project), project_opt); module_vec.pop(); - let module_info = ModuleInfo::from_vec(module_vec); + let module_info = ModuleInfo::from_ty_module(module_vec, None); let project_opt = module_info.parent(); assert_eq!(None, project_opt); } diff --git a/forc-plugins/forc-doc/src/main.rs b/forc-plugins/forc-doc/src/main.rs index 63b9a90ff2..d34c0cad30 100644 --- a/forc-plugins/forc-doc/src/main.rs +++ b/forc-plugins/forc-doc/src/main.rs @@ -82,18 +82,22 @@ pub fn main() -> Result<()> { no_deps, document_private_items, )?; + let root_attributes = + (!typed_program.root.attributes.is_empty()).then_some(typed_program.root.attributes); + let program_kind = typed_program.kind; // render docs to HTML let forc_version = pkg_manifest .project .forc_version .as_ref() .map(|ver| format!("{}.{}.{}", ver.major, ver.minor, ver.patch)); - let rendered_docs = RenderedDocumentation::from(raw_docs, forc_version)?; + let rendered_docs = + RenderedDocumentation::from(raw_docs, root_attributes, program_kind, forc_version)?; // write contents to outfile for doc in rendered_docs.0 { let mut doc_path = doc_path.clone(); - for prefix in doc.module_info.0 { + for prefix in doc.module_info.module_prefixes { if &prefix != project_name { doc_path.push(prefix); } diff --git a/forc-plugins/forc-doc/src/render.rs b/forc-plugins/forc-doc/src/render.rs index a779103543..a89c41f9a3 100644 --- a/forc-plugins/forc-doc/src/render.rs +++ b/forc-plugins/forc-doc/src/render.rs @@ -6,7 +6,7 @@ use std::collections::BTreeMap; use std::fmt::Write; use sway_core::language::ty::{ TyDeclaration::{self, *}, - TyEnumVariant, TyStorageField, TyStructField, TyTraitFn, + TyEnumVariant, TyProgramKind, TyStorageField, TyStructField, TyTraitFn, }; use sway_core::transform::{AttributeKind, AttributesMap}; use sway_lsp::utils::markdown::format_docs; @@ -29,14 +29,22 @@ pub(crate) struct RenderedDocumentation(pub(crate) Vec); impl RenderedDocumentation { /// Top level HTML rendering for all [Documentation] of a program. - pub fn from(raw: Documentation, forc_version: Option) -> Result { + pub fn from( + raw: Documentation, + root_attributes: Option, + program_kind: TyProgramKind, + forc_version: Option, + ) -> Result { let mut rendered_docs: RenderedDocumentation = Default::default(); let root_module = match raw.first() { - Some(doc) => ModuleInfo::from_vec(vec![doc.module_info.project_name().to_owned()]), + Some(doc) => ModuleInfo::from_ty_module( + vec![doc.module_info.project_name().to_owned()], + root_attributes.map(|attrs_map| attrs_map.to_html_string()), + ), None => panic!("Project does not contain a root module"), }; let mut all_docs = DocLinks { - style: DocStyle::AllDoc, + style: DocStyle::AllDoc(program_kind.as_title_str().to_string()), links: Default::default(), }; let mut module_map: BTreeMap>> = @@ -138,7 +146,7 @@ impl RenderedDocumentation { name: location.clone(), module_info: doc.module_info.to_owned(), html_filename: INDEX_FILENAME.to_owned(), - preview_opt: None, + preview_opt: doc.module_info.preview_opt(), }; match module_map.get_mut(parent_module) { Some(doc_links) => match doc_links.get_mut(&BlockTitle::Modules) { @@ -227,7 +235,7 @@ impl RenderedDocumentation { version_opt: forc_version, module_info: root_module.clone(), module_docs: DocLinks { - style: DocStyle::ProjectIndex, + style: DocStyle::ProjectIndex(program_kind.as_title_str().to_string()), links: doc_links.to_owned(), }, } @@ -485,7 +493,7 @@ impl ItemContext { .iter() .map(|field| DocLink { name: field.name.as_str().to_string(), - module_info: ModuleInfo::from_vec(vec![]), + module_info: ModuleInfo::from_ty_module(vec![], None), html_filename: format!( "{}structfield.{}", IDENTITY, @@ -501,7 +509,7 @@ impl ItemContext { .iter() .map(|field| DocLink { name: field.name.as_str().to_string(), - module_info: ModuleInfo::from_vec(vec![]), + module_info: ModuleInfo::from_ty_module(vec![], None), html_filename: format!( "{}storagefield.{}", IDENTITY, @@ -517,7 +525,7 @@ impl ItemContext { .iter() .map(|variant| DocLink { name: variant.name.as_str().to_string(), - module_info: ModuleInfo::from_vec(vec![]), + module_info: ModuleInfo::from_ty_module(vec![], None), html_filename: format!("{}variant.{}", IDENTITY, variant.name.as_str()), preview_opt: None, }) @@ -529,7 +537,7 @@ impl ItemContext { .iter() .map(|method| DocLink { name: method.name.as_str().to_string(), - module_info: ModuleInfo::from_vec(vec![]), + module_info: ModuleInfo::from_ty_module(vec![], None), html_filename: format!( "{}structfield.{}", IDENTITY, @@ -749,7 +757,7 @@ struct DocLinks { impl Renderable for DocLinks { fn render(self) -> Result> { let doc_links = match self.style { - DocStyle::AllDoc => box_html! { + DocStyle::AllDoc(_) => box_html! { @ for (title, list_items) in self.links { @ if !list_items.is_empty() { h3(id=format!("{}", title.html_title_string())) { : title.as_str(); } @@ -777,7 +785,7 @@ impl Renderable for DocLinks { } .into_string() .unwrap(), - DocStyle::ProjectIndex => box_html! { + DocStyle::ProjectIndex(_) => box_html! { @ for (title, list_items) in self.links { @ if !list_items.is_empty() { h3(id=format!("{}", title.html_title_string())) { : title.as_str(); } @@ -914,7 +922,7 @@ impl SidebarNav for AllDocIndex { fn sidebar(&self) -> Sidebar { Sidebar { version_opt: None, - style: DocStyle::AllDoc, + style: self.all_docs.style.clone(), module_info: self.project_name.clone(), href_path: INDEX_FILENAME.to_owned(), nav: self.all_docs.clone(), @@ -989,7 +997,7 @@ pub(crate) struct ModuleIndex { impl SidebarNav for ModuleIndex { fn sidebar(&self) -> Sidebar { let style = match self.module_info.is_root_module() { - true => DocStyle::ProjectIndex, + true => self.module_docs.style.clone(), false => DocStyle::ModuleIndex, }; Sidebar { @@ -1006,8 +1014,8 @@ impl Renderable for ModuleIndex { let doc_links = self.module_docs.clone().render()?; let sidebar = self.sidebar().render()?; let title_prefix = match self.module_docs.style { - DocStyle::ProjectIndex => "Project ", - DocStyle::ModuleIndex => "Module ", + DocStyle::ProjectIndex(ref program_type) => format!("{program_type} "), + DocStyle::ModuleIndex => "Module ".to_string(), _ => unreachable!("Module Index can only be either a project or module at this time."), }; @@ -1079,6 +1087,16 @@ impl Renderable for ModuleIndex { } } } + @ if self.module_info.attributes.is_some() { + details(class="swaydoc-toggle top-doc", open) { + summary(class="hideme") { + span { : "Expand description" } + } + div(class="docblock") { + : Raw(self.module_info.attributes.unwrap()) + } + } + } : doc_links; } } @@ -1094,8 +1112,8 @@ trait SidebarNav { } #[derive(Clone, Ord, PartialOrd, Eq, PartialEq)] enum DocStyle { - AllDoc, - ProjectIndex, + AllDoc(String), + ProjectIndex(String), ModuleIndex, Item, } @@ -1115,8 +1133,8 @@ impl Renderable for Sidebar { .module_info .to_html_shorthand_path_string("assets/sway-logo.svg"); let location_with_prefix = match &self.style { - DocStyle::AllDoc | DocStyle::ProjectIndex => { - format!("Project {}", self.module_info.location()) + DocStyle::AllDoc(project_kind) | DocStyle::ProjectIndex(project_kind) => { + format!("{project_kind} {}", self.module_info.location()) } DocStyle::ModuleIndex | DocStyle::Item => format!( "{} {}", @@ -1125,15 +1143,17 @@ impl Renderable for Sidebar { ), }; let (logo_path_to_parent, path_to_parent_or_self) = match &self.style { - DocStyle::AllDoc | DocStyle::Item => (self.href_path.clone(), self.href_path.clone()), - DocStyle::ProjectIndex => (IDENTITY.to_owned(), IDENTITY.to_owned()), + DocStyle::AllDoc(_) | DocStyle::Item => { + (self.href_path.clone(), self.href_path.clone()) + } + DocStyle::ProjectIndex(_) => (IDENTITY.to_owned(), IDENTITY.to_owned()), DocStyle::ModuleIndex => (format!("../{INDEX_FILENAME}"), IDENTITY.to_owned()), }; // Unfortunately, match arms that return a closure, even if they are the same // type, are incompatible. The work around is to return a String instead, // and render it from Raw in the final output. let styled_content = match &self.style { - DocStyle::ProjectIndex => { + DocStyle::ProjectIndex(_) => { let nav_links = self.nav.links; let version = match self.version_opt { Some(ref v) => v.as_str(), diff --git a/sway-core/src/language/ty/program.rs b/sway-core/src/language/ty/program.rs index 413d216248..d52c025182 100644 --- a/sway-core/src/language/ty/program.rs +++ b/sway-core/src/language/ty/program.rs @@ -465,6 +465,15 @@ impl TyProgramKind { TyProgramKind::Script { .. } => parsed::TreeType::Script, } } + /// Used for project titles in `forc doc`. + pub fn as_title_str(&self) -> &str { + match self { + TyProgramKind::Contract { .. } => "Contract", + TyProgramKind::Library { .. } => "Library", + TyProgramKind::Predicate { .. } => "Predicate", + TyProgramKind::Script { .. } => "Script", + } + } } fn disallow_impure_functions( diff --git a/sway-lib-std/src/lib.sw b/sway-lib-std/src/lib.sw index 6b7f994d77..3e313d5368 100644 --- a/sway-lib-std/src/lib.sw +++ b/sway-lib-std/src/lib.sw @@ -1,3 +1,4 @@ +//! The official standard library for the Sway smart contract language. library std; dep error_signals; diff --git a/sway-lsp/tests/lib.rs b/sway-lsp/tests/lib.rs index a4a58e06d2..ede363124e 100644 --- a/sway-lsp/tests/lib.rs +++ b/sway-lsp/tests/lib.rs @@ -498,7 +498,7 @@ async fn go_to_definition_for_paths() { req_uri: &uri, req_line: 10, req_char: 13, - def_line: 0, + def_line: 1, def_start_char: 8, def_end_char: 11, def_path: "sway-lib-std/src/lib.sw", diff --git a/sway-parse/src/lib.rs b/sway-parse/src/lib.rs index 5c09d01f70..55c98e3669 100644 --- a/sway-parse/src/lib.rs +++ b/sway-parse/src/lib.rs @@ -25,7 +25,11 @@ pub use crate::{ token::{lex, lex_commented}, }; -use sway_ast::{attribute::Annotated, Module, ModuleKind}; +use sway_ast::{ + attribute::Annotated, + token::{DocComment, DocStyle}, + Module, ModuleKind, +}; use sway_error::handler::{ErrorEmitted, Handler}; use std::{path::PathBuf, sync::Arc}; @@ -45,5 +49,13 @@ pub fn parse_module_kind( path: Option>, ) -> Result { let ts = lex(handler, &src, 0, src.len(), path)?; - Parser::new(handler, &ts).parse() + let mut parser = Parser::new(handler, &ts); + while let Some(DocComment { + doc_style: DocStyle::Inner, + .. + }) = parser.peek() + { + parser.parse::()?; + } + parser.parse() }