mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 23:51:31 +03:00
Documentation panel redesign (#7372)
* Support arguments list in the doc parser * Support new doc sections in documentation panel * Remove headers * Remove outer dom and place breadcrumbs * Fix methods icon * Use unordered list class in css * Improve tags styles * Remove virtual component groups docs * Cleanup top-level css styles * Small adjustments to headers * Add styles for emphasized text * Add bold font for arguments * Self-review * Remove redundant placeholder struct. * Update outdated doc. * Avoid allocation when comparing strings. * Avoid empty paragraph. * Reduce allocations. * Update test to remove empty paragraph. * Fix rebase issues. * Improve padding and size handling in UI themes Added padding_x and padding_y to hardcoded theme's breadcrumb settings to ensure consistent padding. Also, these padding settings and breadcrumb_height are now used directly in the Style structure, eliminating hardcoded values in the view documentation. * Adjusted breadcrumb background dimensions calculation. * Add support for improper arguments formatting in documenation comments * Do not include Icon tag into the docs * Fix documentation panel resizing * enso-formatter --------- Co-authored-by: Michael Mauderer <michael.mauderer@enso.org>
This commit is contained in:
parent
7f19b09d13
commit
02ba9a1a11
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2113,6 +2113,7 @@ dependencies = [
|
||||
"enso-profiler",
|
||||
"enso-reflect",
|
||||
"lexpr",
|
||||
"pretty_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -29,10 +29,20 @@ use crate::SuggestionDatabase;
|
||||
use double_representation::name::QualifiedName;
|
||||
use enso_doc_parser::DocSection;
|
||||
use enso_doc_parser::Mark;
|
||||
use enso_doc_parser::Tag as DocSectionTag;
|
||||
use std::cmp::Ordering;
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Constants ===
|
||||
// =================
|
||||
|
||||
/// A list of tags which are not included into generated documentation.
|
||||
const IGNORED_TAGS: &[DocSectionTag] = &[DocSectionTag::Icon];
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Errors ===
|
||||
// ==============
|
||||
@ -54,14 +64,14 @@ pub struct NoParentModule(String);
|
||||
#[derive(Debug, PartialEq, From, Clone, CloneRef)]
|
||||
pub enum EntryDocumentation {
|
||||
/// No documentation available.
|
||||
Placeholder(Placeholder),
|
||||
Placeholder,
|
||||
/// Documentation for the entry.
|
||||
Docs(Documentation),
|
||||
}
|
||||
|
||||
impl Default for EntryDocumentation {
|
||||
fn default() -> Self {
|
||||
Placeholder::NoDocumentation.into()
|
||||
EntryDocumentation::Placeholder
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +109,7 @@ impl EntryDocumentation {
|
||||
},
|
||||
Err(_) => {
|
||||
error!("No entry found for id: {id:?}");
|
||||
Self::Placeholder(Placeholder::NoDocumentation)
|
||||
EntryDocumentation::Placeholder
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
@ -194,7 +204,7 @@ impl EntryDocumentation {
|
||||
Documentation::Local(_) => default(),
|
||||
Documentation::Builtin(_) => default(),
|
||||
},
|
||||
EntryDocumentation::Placeholder(_) => default(),
|
||||
EntryDocumentation::Placeholder => default(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,7 +242,7 @@ impl EntryDocumentation {
|
||||
Some(self_type) => self_type,
|
||||
None => {
|
||||
error!("Method without self type: {}.", entry.qualified_name());
|
||||
return Ok(Placeholder::NoDocumentation.into());
|
||||
return Ok(EntryDocumentation::Placeholder);
|
||||
}
|
||||
};
|
||||
let self_type = db.lookup_by_qualified_name(self_type);
|
||||
@ -251,12 +261,12 @@ impl EntryDocumentation {
|
||||
}
|
||||
_ => {
|
||||
error!("Unexpected parent kind for method {}.", entry.qualified_name());
|
||||
Ok(Placeholder::NoDocumentation.into())
|
||||
Ok(EntryDocumentation::Placeholder)
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Parent entry for method {} not found: {}", entry.qualified_name(), err);
|
||||
Ok(Self::Placeholder(Placeholder::NoDocumentation))
|
||||
Ok(EntryDocumentation::Placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -277,23 +287,12 @@ impl EntryDocumentation {
|
||||
}
|
||||
Err(err) => {
|
||||
error!("No return type found for constructor {}: {}", entry.qualified_name(), err);
|
||||
Ok(Placeholder::NoDocumentation.into())
|
||||
Ok(EntryDocumentation::Placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Placeholder ===
|
||||
|
||||
/// No documentation is available for the entry.
|
||||
#[derive(Debug, Clone, CloneRef, PartialEq)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Placeholder {
|
||||
/// Documentation is empty.
|
||||
NoDocumentation,
|
||||
/// Documentation for the Virtual Component group.
|
||||
VirtualComponentGroup { name: ImString },
|
||||
}
|
||||
|
||||
// === Documentation ===
|
||||
|
||||
@ -709,6 +708,8 @@ impl<'a, T: Clone + PartialOrd + Ord> From<&'a [T]> for SortedVec<T> {
|
||||
|
||||
/// Helper structure for splitting entry's [`DocSection`]s into [`Tags`], [`Synopsis`], and
|
||||
/// [`Examples`].
|
||||
///
|
||||
/// Skips [`Tags`] from the [`IGNORED_TAGS`] list.
|
||||
struct FilteredDocSections {
|
||||
tags: Tags,
|
||||
synopsis: Synopsis,
|
||||
@ -723,7 +724,10 @@ impl FilteredDocSections {
|
||||
let mut examples = Vec::new();
|
||||
for section in doc_sections {
|
||||
match section {
|
||||
DocSection::Tag { tag, body } => tags.push(Tag::new(tag.to_str(), body)),
|
||||
DocSection::Tag { tag, body } =>
|
||||
if !IGNORED_TAGS.contains(tag) {
|
||||
tags.push(Tag::new(tag.to_str(), body));
|
||||
},
|
||||
DocSection::Marked { mark: Mark::Example, .. } => examples.push(section.clone()),
|
||||
section => synopsis.push(section.clone()),
|
||||
}
|
||||
@ -754,7 +758,7 @@ mod tests {
|
||||
// Arbitrary non-existing entry id.
|
||||
let entry_id = 10;
|
||||
let docs = EntryDocumentation::new(&db, &entry_id).unwrap();
|
||||
assert_eq!(docs, EntryDocumentation::Placeholder(Placeholder::NoDocumentation));
|
||||
assert_eq!(docs, EntryDocumentation::Placeholder);
|
||||
}
|
||||
|
||||
fn assert_docs(db: &SuggestionDatabase, name: Rc<QualifiedName>, expected: Documentation) {
|
||||
|
@ -425,6 +425,18 @@ macro_rules! doc_section_mark {
|
||||
/// doc_section!(> "Marked as example");
|
||||
/// doc_section!(> "Optional header", "Marked as example");
|
||||
/// ```
|
||||
///
|
||||
/// ### [`DocSection::List`]
|
||||
/// ```
|
||||
/// # use enso_suggestion_database::doc_section;
|
||||
/// doc_section!(- "Item 1"; - "Item 2"; -"Item 3");
|
||||
/// ```
|
||||
///
|
||||
/// ### [`DocSection::Arguments`]
|
||||
/// ```
|
||||
/// # use enso_suggestion_database::doc_section;
|
||||
/// doc_section!(- "arg_name", "Description"; - "arg_name2", "Description2");
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! doc_section {
|
||||
(@ $tag:ident, $body:expr) => {
|
||||
@ -433,6 +445,16 @@ macro_rules! doc_section {
|
||||
body: $body.into(),
|
||||
}
|
||||
};
|
||||
($(- $body:expr);*) => {
|
||||
$crate::mock::enso_doc_parser::DocSection::List { items: vec![
|
||||
$($body.into()),*
|
||||
]}
|
||||
};
|
||||
($(- $name:expr, $desc:expr);*) => {
|
||||
$crate::mock::enso_doc_parser::DocSection::Arguments { args: vec![
|
||||
$($crate::mock::enso_doc_parser::Argument { name: $name.into(), description: $desc.into() }),*
|
||||
]}
|
||||
};
|
||||
($mark:tt $body:expr) => {
|
||||
$crate::mock::enso_doc_parser::DocSection::Marked {
|
||||
mark: $crate::doc_section_mark!($mark),
|
||||
|
@ -1,5 +1,4 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="32" height="32" fill="white"/>
|
||||
<mask id="path-1-inside-1_7717_245722" fill="white">
|
||||
<rect x="2" y="2" width="13" height="13" rx="2"/>
|
||||
</mask>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@ -1,7 +0,0 @@
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 16C3.58172 16 1.93129e-07 12.4183 0 8C-1.93129e-07 3.58172 3.58172 1.93129e-07 8 0H24C28.4183 -1.93129e-07 32 3.58172 32 8V24C32 28.4183 28.4183 32 24 32C19.5817 32 16 28.4183 16 24C16 19.5817 12.4183 16 8 16Z" fill="#9640DA" fill-opacity="0.3"/>
|
||||
<ellipse cx="24" cy="24" rx="4" ry="4" transform="rotate(-90 24 24)" fill="#9640DA"/>
|
||||
<ellipse cx="24" cy="8" rx="4" ry="4" transform="rotate(-90 24 8)" fill="#9640DA"/>
|
||||
<ellipse cx="8" cy="8" rx="4" ry="4" transform="rotate(-90 8 8)" fill="#9640DA"/>
|
||||
<circle cx="8" cy="24" r="5" transform="rotate(-90 8 24)" fill="#9640DA" fill-opacity="0.5"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 673 B |
@ -14,7 +14,8 @@
|
||||
--enso-docs-example-background-color: #e6f1f8;
|
||||
--enso-docs-background-color: #eaeaea;
|
||||
--enso-docs-text-color: #434343;
|
||||
--enso-docs-tag-background-color: #f5f5f5;
|
||||
--enso-docs-tag-background-color: #dcd8d8;
|
||||
--enso-docs-code-background-color: #dddcde;
|
||||
--enso-docs-caption-background-color: #0077f6;
|
||||
}
|
||||
|
||||
@ -23,32 +24,30 @@
|
||||
font-size: 11.5px;
|
||||
color: var(--enso-docs-text-color);
|
||||
background-color: var(--enso-docs-background-color);
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
/* It's better to use `.unordered-list` class here instead of `ul`, but it will break the keyed lists that a provided by the engine.
|
||||
* We need a separate way to represent keyed lists without embedded HTML tags. */
|
||||
.enso-docs ul {
|
||||
.enso-docs .unordered-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
.enso-docs ul li:before {
|
||||
.enso-docs .unordered-list li:before {
|
||||
content: "•";
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.enso-docs ul li.type-item:before {
|
||||
.enso-docs .unordered-list li.type-item:before {
|
||||
color: var(--enso-docs-type-name-color);
|
||||
}
|
||||
|
||||
.enso-docs ul li.method-item:before {
|
||||
.enso-docs .unordered-list li.method-item:before {
|
||||
color: var(--enso-docs-method-name-color);
|
||||
}
|
||||
|
||||
@ -87,19 +86,17 @@
|
||||
opacity: 0.34;
|
||||
}
|
||||
|
||||
.enso-docs .argument {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.enso-docs .section-content {
|
||||
padding-left: 1.25rem;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
/* Headers. */
|
||||
|
||||
.enso-docs .header-top {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: var(--enso-docs-type-name-color);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
div.enso-docs .header-icon {
|
||||
align-self: start;
|
||||
display: flex;
|
||||
@ -116,6 +113,8 @@ div.enso-docs .header-icon {
|
||||
}
|
||||
|
||||
.enso-docs .header-container {
|
||||
padding-left: 8px;
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@ -201,6 +200,14 @@ div.enso-docs .marked-icon-info {
|
||||
margin: 0.05rem 0.1rem;
|
||||
}
|
||||
|
||||
/* Code. The words emphasized with backticks. */
|
||||
|
||||
.enso-docs code {
|
||||
background-color: var(--enso-docs-code-background-color);
|
||||
border-radius: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
|
||||
.enso-docs .tags-container {
|
||||
@ -210,9 +217,13 @@ div.enso-docs .marked-icon-info {
|
||||
}
|
||||
|
||||
.enso-docs .tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
background-color: var(--enso-docs-tag-background-color);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
margin-bottom: 1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ use enso_suggestion_database::documentation_ir::Examples;
|
||||
use enso_suggestion_database::documentation_ir::Function;
|
||||
use enso_suggestion_database::documentation_ir::LocalDocumentation;
|
||||
use enso_suggestion_database::documentation_ir::ModuleDocumentation;
|
||||
use enso_suggestion_database::documentation_ir::Placeholder;
|
||||
use enso_suggestion_database::documentation_ir::Synopsis;
|
||||
use enso_suggestion_database::documentation_ir::Tag;
|
||||
use enso_suggestion_database::documentation_ir::TypeDocumentation;
|
||||
@ -33,7 +32,6 @@ use horrorshow::owned_html;
|
||||
|
||||
/// We use SVG icons imported as text.
|
||||
type Icon = &'static str;
|
||||
const ICON_TYPE: &str = include_str!("../assets/icon-type.svg");
|
||||
const ICON_METHODS: &str = include_str!("../assets/icon-methods.svg");
|
||||
const ICON_EXAMPLES: &str = include_str!("../assets/icon-examples.svg");
|
||||
const ICON_INFO: &str = include_str!("../assets/icon-info.svg");
|
||||
@ -58,11 +56,7 @@ fn svg_icon(content: &'static str, class: &'static str) -> impl Render {
|
||||
#[profile(Detail)]
|
||||
pub fn render(docs: &EntryDocumentation) -> String {
|
||||
let html = match docs {
|
||||
EntryDocumentation::Placeholder(placeholder) => match placeholder {
|
||||
Placeholder::NoDocumentation => String::from("No documentation available."),
|
||||
Placeholder::VirtualComponentGroup { name } =>
|
||||
render_virtual_component_group_docs(name.clone_ref()),
|
||||
},
|
||||
EntryDocumentation::Placeholder => String::from("No documentation available."),
|
||||
EntryDocumentation::Docs(docs) => render_documentation(docs.clone_ref()),
|
||||
};
|
||||
match validate_utf8(&html) {
|
||||
@ -81,58 +75,18 @@ fn validate_utf8(s: &str) -> Result<&str, std::str::Utf8Error> {
|
||||
}
|
||||
|
||||
fn render_documentation(docs: Documentation) -> String {
|
||||
let back_link = match &docs {
|
||||
Documentation::Constructor { type_docs, .. } => Some(BackLink {
|
||||
displayed: type_docs.name.name().to_owned(),
|
||||
id: anchor_name(&type_docs.name),
|
||||
}),
|
||||
Documentation::Method { type_docs, .. } => Some(BackLink {
|
||||
displayed: type_docs.name.name().to_owned(),
|
||||
id: anchor_name(&type_docs.name),
|
||||
}),
|
||||
Documentation::ModuleMethod { module_docs, .. } => Some(BackLink {
|
||||
displayed: module_docs.name.name().to_owned(),
|
||||
id: anchor_name(&module_docs.name),
|
||||
}),
|
||||
Documentation::Type { module_docs, .. } => Some(BackLink {
|
||||
displayed: module_docs.name.name().to_owned(),
|
||||
id: anchor_name(&module_docs.name),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
match docs {
|
||||
Documentation::Module(module_docs) => render_module_documentation(&module_docs),
|
||||
Documentation::Type { docs, .. } => render_type_documentation(&docs, back_link),
|
||||
Documentation::Function(docs) => render_function_documentation(&docs, back_link),
|
||||
Documentation::Type { docs, .. } => render_type_documentation(&docs),
|
||||
Documentation::Function(docs) => render_function_documentation(&docs),
|
||||
Documentation::Local(docs) => render_local_documentation(&docs),
|
||||
Documentation::Constructor { docs, .. } => render_function_documentation(&docs, back_link),
|
||||
Documentation::Method { docs, .. } => render_function_documentation(&docs, back_link),
|
||||
Documentation::ModuleMethod { docs, .. } => render_function_documentation(&docs, back_link),
|
||||
Documentation::Constructor { docs, .. } => render_function_documentation(&docs),
|
||||
Documentation::Method { docs, .. } => render_function_documentation(&docs),
|
||||
Documentation::ModuleMethod { docs, .. } => render_function_documentation(&docs),
|
||||
Documentation::Builtin(builtin_docs) => render_builtin_documentation(&builtin_docs),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the documentation of the virtual component group. Includes the name of the group.
|
||||
fn render_virtual_component_group_docs(name: ImString) -> String {
|
||||
let content = owned_html! {
|
||||
h1(class="header-top") {
|
||||
: &*name
|
||||
}
|
||||
};
|
||||
docs_content(content).into_string().unwrap()
|
||||
}
|
||||
|
||||
/// An optional link to the parent entry (module or type), that is displayed in the documentation
|
||||
/// header. Pressing this link will switch the documentation to the parent entry, allowing
|
||||
/// bidirectional navigation.
|
||||
#[derive(Debug, Clone)]
|
||||
struct BackLink {
|
||||
/// Displayed text.
|
||||
displayed: String,
|
||||
/// The unique ID of the link.
|
||||
id: String,
|
||||
}
|
||||
|
||||
|
||||
// === Types ===
|
||||
|
||||
@ -143,12 +97,10 @@ struct BackLink {
|
||||
/// - Synopsis and a list of constructors.
|
||||
/// - Methods.
|
||||
/// - Examples.
|
||||
fn render_type_documentation(docs: &TypeDocumentation, back_link: Option<BackLink>) -> String {
|
||||
fn render_type_documentation(docs: &TypeDocumentation) -> String {
|
||||
let constructors_exist = !docs.constructors.is_empty();
|
||||
let methods_exist = !docs.methods.is_empty();
|
||||
let examples_exist = !docs.examples.is_empty();
|
||||
let name = &docs.name;
|
||||
let arguments = &docs.arguments;
|
||||
let synopsis = &docs.synopsis;
|
||||
let constructors = &docs.constructors;
|
||||
let synopsis = section_content(type_synopsis(synopsis));
|
||||
@ -156,11 +108,8 @@ fn render_type_documentation(docs: &TypeDocumentation, back_link: Option<BackLin
|
||||
let methods = section_content(list_of_functions(&docs.methods));
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let tags = section_content(list_of_tags(&docs.tags));
|
||||
let header_text = type_header(name.name(), arguments_list(arguments), back_link.as_ref());
|
||||
let header = header(ICON_TYPE, header_text, "header-top");
|
||||
|
||||
let content = owned_html! {
|
||||
: &header;
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if constructors_exist {
|
||||
@ -195,24 +144,6 @@ fn types_header() -> impl Render {
|
||||
header(ICON_METHODS, "Types", "section-header types-header")
|
||||
}
|
||||
|
||||
/// A header for the type documentation.
|
||||
fn type_header<'a>(
|
||||
name: &'a str,
|
||||
arguments: impl Render + 'a,
|
||||
back_link: Option<&'a BackLink>,
|
||||
) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ if let Some(BackLink { id, displayed }) = &back_link {
|
||||
a(id=id, class="link") {
|
||||
: displayed;
|
||||
}
|
||||
: ".";
|
||||
}
|
||||
: &name;
|
||||
span(class="arguments") { : &arguments }
|
||||
}
|
||||
}
|
||||
|
||||
/// A synopsis of the type. Contains a list of constructors, if it is not empty.
|
||||
fn type_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ -269,14 +200,12 @@ fn render_module_documentation(docs: &ModuleDocumentation) -> String {
|
||||
let types_exist = !docs.types.is_empty();
|
||||
let methods_exist = !docs.methods.is_empty();
|
||||
let examples_exist = !docs.examples.is_empty();
|
||||
let name = &docs.name;
|
||||
let synopsis = section_content(module_synopsis(&docs.synopsis));
|
||||
let types = section_content(list_of_types(&docs.types));
|
||||
let methods = section_content(list_of_functions(&docs.methods));
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let tags = section_content(list_of_tags(&docs.tags));
|
||||
let content = owned_html! {
|
||||
: header(ICON_TYPE, name.name(), "header-top");
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if types_exist {
|
||||
@ -352,17 +281,14 @@ fn module_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
|
||||
// === Functions ===
|
||||
|
||||
/// Render documentation of a function.
|
||||
fn render_function_documentation(docs: &Function, back_link: Option<BackLink>) -> String {
|
||||
let Function { name, arguments, synopsis, tags, .. } = docs;
|
||||
fn render_function_documentation(docs: &Function) -> String {
|
||||
let Function { synopsis, tags, .. } = docs;
|
||||
|
||||
let examples_exist = !docs.examples.is_empty();
|
||||
let synopsis = section_content(function_synopsis(synopsis));
|
||||
let tags = section_content(list_of_tags(tags));
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let header_text = function_header(name.name(), arguments_list(arguments), back_link.as_ref());
|
||||
let header = header(ICON_TYPE, header_text, "header-top");
|
||||
let content = owned_html! {
|
||||
: &header;
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if examples_exist {
|
||||
@ -373,24 +299,6 @@ fn render_function_documentation(docs: &Function, back_link: Option<BackLink>) -
|
||||
docs_content(content).into_string().unwrap()
|
||||
}
|
||||
|
||||
/// A header for the function documentation.
|
||||
fn function_header<'a>(
|
||||
name: &'a str,
|
||||
arguments: impl Render + 'a,
|
||||
back_link: Option<&'a BackLink>,
|
||||
) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ if let Some(BackLink { id, displayed }) = &back_link {
|
||||
a(id=id, class="link") {
|
||||
: displayed;
|
||||
}
|
||||
: ".";
|
||||
}
|
||||
: name;
|
||||
span(class="arguments") { : &arguments }
|
||||
}
|
||||
}
|
||||
|
||||
/// A synopsis of the function.
|
||||
fn function_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ -405,17 +313,14 @@ fn function_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
|
||||
|
||||
/// Render documentation of a function.
|
||||
fn render_local_documentation(docs: &LocalDocumentation) -> String {
|
||||
let LocalDocumentation { name, synopsis, return_type, tags, .. } = docs;
|
||||
let LocalDocumentation { synopsis, tags, .. } = docs;
|
||||
|
||||
let examples_exist = !docs.examples.is_empty();
|
||||
let synopsis = section_content(local_synopsis(synopsis));
|
||||
let tags = section_content(list_of_tags(tags));
|
||||
let examples = section_content(list_of_examples(&docs.examples));
|
||||
let header_text = local_header(name.name(), return_type.name());
|
||||
let header = header(ICON_TYPE, header_text, "header-top");
|
||||
|
||||
let content = owned_html! {
|
||||
: &header;
|
||||
: &tags;
|
||||
: &synopsis;
|
||||
@ if examples_exist {
|
||||
@ -426,14 +331,6 @@ fn render_local_documentation(docs: &LocalDocumentation) -> String {
|
||||
docs_content(content).into_string().unwrap()
|
||||
}
|
||||
|
||||
/// A header for the local documentation.
|
||||
fn local_header<'a>(name: &'a str, return_type: &'a str) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
: name; : " ";
|
||||
span(class="arguments") { : return_type }
|
||||
}
|
||||
}
|
||||
|
||||
/// A synopsis of the local.
|
||||
fn local_synopsis<'a>(synopsis: &'a Synopsis) -> Box<dyn Render + 'a> {
|
||||
box_html! {
|
||||
@ -550,6 +447,27 @@ fn paragraph<'a>(doc_section: &'a DocSection) -> Box<dyn Render + 'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
DocSection::List { items } => {
|
||||
box_html! {
|
||||
ul(class="unordered-list") {
|
||||
@for item in items {
|
||||
li { : Raw(&item); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DocSection::Arguments { args } => {
|
||||
box_html! {
|
||||
ul(class="unordered-list") {
|
||||
@for arg in args {
|
||||
li {
|
||||
span(class="argument") { : &arg.name; }
|
||||
: ": "; : Raw(&arg.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => box_html! {
|
||||
p(class="paragraph") { : "Unexpected doc section type." }
|
||||
},
|
||||
|
@ -1,5 +1,4 @@
|
||||
//! Documentation view visualization generating and presenting Enso Documentation under
|
||||
//! the documented node.
|
||||
//! Documentation view presenting the documentation in the Component Browser.
|
||||
|
||||
#![recursion_limit = "1024"]
|
||||
// === Features ===
|
||||
@ -65,10 +64,16 @@ const DISPLAY_DELAY_MS: i32 = 0;
|
||||
#[base_path = "theme"]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Style {
|
||||
width: f32,
|
||||
height: f32,
|
||||
background: color::Rgba,
|
||||
corner_radius: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
background: color::Rgba,
|
||||
corner_radius: f32,
|
||||
#[theme_path = "theme::breadcrumbs::height"]
|
||||
breadcrumbs_height: f32,
|
||||
#[theme_path = "theme::breadcrumbs::padding_x"]
|
||||
breadcrumbs_padding_x: f32,
|
||||
#[theme_path = "theme::breadcrumbs::padding_y"]
|
||||
breadcrumbs_padding_y: f32,
|
||||
}
|
||||
|
||||
|
||||
@ -81,12 +86,13 @@ pub struct Style {
|
||||
#[derive(Clone, CloneRef, Debug, display::Object)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Model {
|
||||
outer_dom: DomSymbol,
|
||||
inner_dom: DomSymbol,
|
||||
style_container: DomSymbol,
|
||||
dom: DomSymbol,
|
||||
pub breadcrumbs: breadcrumbs::Breadcrumbs,
|
||||
/// The purpose of this overlay is stop propagating mouse events under the documentation panel
|
||||
/// to EnsoGL shapes, and pass them to the DOM instead.
|
||||
overlay: Rectangle,
|
||||
background: Rectangle,
|
||||
display_object: display::object::Instance,
|
||||
event_handlers: Rc<RefCell<Vec<web::EventListenerHandle>>>,
|
||||
}
|
||||
@ -96,10 +102,11 @@ impl Model {
|
||||
fn new(app: &Application) -> Self {
|
||||
let scene = &app.display.default_scene;
|
||||
let display_object = display::object::Instance::new();
|
||||
let outer_div = web::document.create_div_or_panic();
|
||||
let outer_dom = DomSymbol::new(&outer_div);
|
||||
let inner_div = web::document.create_div_or_panic();
|
||||
let inner_dom = DomSymbol::new(&inner_div);
|
||||
let style_div = web::document.create_div_or_panic();
|
||||
let style_container = DomSymbol::new(&style_div);
|
||||
let div = web::document.create_div_or_panic();
|
||||
let dom = DomSymbol::new(&div);
|
||||
let background = Rectangle::new();
|
||||
let overlay = Rectangle::new().build(|r| {
|
||||
r.set_color(INVISIBLE_HOVER_COLOR);
|
||||
});
|
||||
@ -108,28 +115,26 @@ impl Model {
|
||||
breadcrumbs.set_base_layer(&app.display.default_scene.layers.node_searcher);
|
||||
display_object.add_child(&breadcrumbs);
|
||||
|
||||
outer_dom.dom().set_style_or_warn("white-space", "normal");
|
||||
outer_dom.dom().set_style_or_warn("overflow-y", "auto");
|
||||
outer_dom.dom().set_style_or_warn("overflow-x", "auto");
|
||||
outer_dom.dom().set_style_or_warn("pointer-events", "auto");
|
||||
dom.dom().set_attribute_or_warn("class", "scrollable");
|
||||
dom.dom().set_style_or_warn("white-space", "normal");
|
||||
dom.dom().set_style_or_warn("overflow-y", "auto");
|
||||
dom.dom().set_style_or_warn("overflow-x", "auto");
|
||||
dom.dom().set_style_or_warn("pointer-events", "auto");
|
||||
|
||||
inner_dom.dom().set_attribute_or_warn("class", "scrollable");
|
||||
inner_dom.dom().set_style_or_warn("white-space", "normal");
|
||||
inner_dom.dom().set_style_or_warn("overflow-y", "auto");
|
||||
inner_dom.dom().set_style_or_warn("overflow-x", "auto");
|
||||
inner_dom.dom().set_style_or_warn("pointer-events", "auto");
|
||||
|
||||
display_object.add_child(&outer_dom);
|
||||
outer_dom.add_child(&inner_dom);
|
||||
display_object.add_child(&background);
|
||||
display_object.add_child(&style_container);
|
||||
display_object.add_child(&dom);
|
||||
display_object.add_child(&overlay);
|
||||
scene.dom.layers.node_searcher.manage(&outer_dom);
|
||||
scene.dom.layers.node_searcher.manage(&inner_dom);
|
||||
|
||||
scene.dom.layers.node_searcher.manage(&style_container);
|
||||
scene.dom.layers.node_searcher.manage(&dom);
|
||||
|
||||
Model {
|
||||
outer_dom,
|
||||
inner_dom,
|
||||
style_container,
|
||||
dom,
|
||||
breadcrumbs,
|
||||
overlay,
|
||||
background,
|
||||
display_object,
|
||||
event_handlers: default(),
|
||||
}
|
||||
@ -141,12 +146,12 @@ impl Model {
|
||||
self
|
||||
}
|
||||
|
||||
/// Add `<style>` tag with the stylesheet to the `outer_dom`.
|
||||
/// Add `<style>` tag with the stylesheet to the `style_container`.
|
||||
fn load_css_stylesheet(&self) {
|
||||
let stylesheet = include_str!("../assets/styles.css");
|
||||
let element = web::document.create_element_or_panic("style");
|
||||
element.set_inner_html(stylesheet);
|
||||
self.outer_dom.append_or_warn(&element);
|
||||
self.style_container.append_or_warn(&element);
|
||||
}
|
||||
|
||||
fn set_initial_breadcrumbs(&self) {
|
||||
@ -156,35 +161,35 @@ impl Model {
|
||||
}
|
||||
|
||||
/// Set size of the documentation view.
|
||||
fn size_changed(&self, size: Vector2, fraction: f32) {
|
||||
let visible_part = Vector2(size.x * fraction, size.y);
|
||||
self.outer_dom.set_xy(size / 2.0);
|
||||
fn size_changed(&self, size: Vector2, width_fraction: f32, style: &Style) {
|
||||
let visible_part = Vector2(size.x * width_fraction, size.y);
|
||||
let dom_size =
|
||||
Vector2(size.x, size.y - style.breadcrumbs_height - style.breadcrumbs_padding_y);
|
||||
self.dom.set_dom_size(dom_size);
|
||||
self.dom.set_xy(dom_size / 2.0);
|
||||
self.overlay.set_size(visible_part);
|
||||
self.outer_dom.set_dom_size(Vector2(size.x, size.y));
|
||||
self.inner_dom.set_dom_size(Vector2(size.x, size.y));
|
||||
self.breadcrumbs.set_xy(Vector2(0.0, size.y + 36.0));
|
||||
self.breadcrumbs.frp().set_size(Vector2(visible_part.x, 32.0));
|
||||
self.breadcrumbs.set_xy(Vector2(style.breadcrumbs_padding_x, size.y));
|
||||
self.breadcrumbs.frp().set_size(Vector2(visible_part.x, style.breadcrumbs_height));
|
||||
self.background.set_size(visible_part);
|
||||
}
|
||||
|
||||
/// Set the fraction of visible documentation panel. Used to animate showing/hiding the panel.
|
||||
fn width_animation_changed(&self, style: &Style, size: Vector2, fraction: f32) {
|
||||
let percentage = (1.0 - fraction) * 100.0;
|
||||
let clip_path = format!("inset(0 {percentage}% 0 0 round {}px)", style.corner_radius);
|
||||
self.inner_dom.set_style_or_warn("clip-path", &clip_path);
|
||||
self.outer_dom.set_style_or_warn("clip-path", &clip_path);
|
||||
let actual_size = Vector2(size.x * fraction, size.y);
|
||||
self.overlay.set_size(actual_size);
|
||||
self.breadcrumbs.frp().set_size(Vector2(actual_size.x, 32.0));
|
||||
let clip_path =
|
||||
format!("inset(0 {percentage}% 0 0 round 0px 0px {0}px {0}px)", style.corner_radius);
|
||||
self.dom.set_style_or_warn("clip-path", clip_path);
|
||||
self.size_changed(size, fraction, style);
|
||||
}
|
||||
|
||||
/// Display the documentation and scroll to the qualified name if needed.
|
||||
/// Display the documentation and scroll to default position.
|
||||
fn display_doc(&self, docs: EntryDocumentation, display_doc: &frp::Source<EntryDocumentation>) {
|
||||
let linked_pages = docs.linked_doc_pages();
|
||||
let html = html::render(&docs);
|
||||
self.inner_dom.dom().set_inner_html(&html);
|
||||
self.dom.dom().set_inner_html(&html);
|
||||
self.set_link_handlers(linked_pages, display_doc);
|
||||
// Scroll to the top of the page.
|
||||
self.inner_dom.dom().set_scroll_top(0);
|
||||
self.dom.dom().set_scroll_top(0);
|
||||
}
|
||||
|
||||
/// Setup event handlers for links on the documentation page.
|
||||
@ -212,16 +217,16 @@ impl Model {
|
||||
/// TODO(#5214): This should be replaced with a EnsoGL spinner.
|
||||
fn load_waiting_screen(&self) {
|
||||
let spinner = include_str!("../assets/spinner.html");
|
||||
self.inner_dom.dom().set_inner_html(spinner)
|
||||
self.dom.dom().set_inner_html(spinner)
|
||||
}
|
||||
|
||||
fn update_style(&self, style: Style) {
|
||||
// Size is updated separately in [`size_changed`] method.
|
||||
self.overlay.set_corner_radius(style.corner_radius);
|
||||
self.outer_dom.set_style_or_warn("border-radius", format!("{}px", style.corner_radius));
|
||||
self.inner_dom.set_style_or_warn("border-radius", format!("{}px", style.corner_radius));
|
||||
let bg_color = style.background.to_javascript_string();
|
||||
self.outer_dom.set_style_or_warn("background-color", bg_color);
|
||||
self.dom.set_style_or_warn("border-radius", format!("{}px", style.corner_radius));
|
||||
|
||||
self.background.set_color(style.background);
|
||||
self.background.set_corner_radius(style.corner_radius);
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,15 +277,6 @@ pub struct View {
|
||||
}
|
||||
|
||||
impl View {
|
||||
/// Definition of this visualization.
|
||||
pub fn definition() -> visualization::Definition {
|
||||
let path = visualization::Path::builtin("Documentation View");
|
||||
visualization::Definition::new(
|
||||
visualization::Signature::new_for_any_type(path, visualization::Format::Json),
|
||||
|app| Ok(Self::new(app).into()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Constructor.
|
||||
pub fn new(app: &Application) -> Self {
|
||||
let frp = Frp::new();
|
||||
@ -319,7 +315,6 @@ impl View {
|
||||
// === Size ===
|
||||
|
||||
size <- style.map(|s| Vector2(s.width, s.height));
|
||||
_eval <- size.map2(&width_anim.value, f!((&s, &f) model.size_changed(s, f)));
|
||||
|
||||
|
||||
// === Style ===
|
||||
@ -331,7 +326,8 @@ impl View {
|
||||
|
||||
width_anim.target <+ frp.set_visible.map(|&visible| if visible { 1.0 } else { 0.0 });
|
||||
width_anim.skip <+ frp.skip_animation;
|
||||
_eval <- width_anim.value.map3(&size, &style, f!((&f, &sz, st) model.width_animation_changed(st, sz, f)));
|
||||
size_change <- all3(&width_anim.value, &size, &style);
|
||||
eval size_change(((f, sz, st)) model.width_animation_changed(st, *sz, *f));
|
||||
|
||||
|
||||
// === Activation ===
|
||||
@ -371,6 +367,11 @@ impl View {
|
||||
|
||||
impl From<View> for visualization::Instance {
|
||||
fn from(t: View) -> Self {
|
||||
Self::new(&t, &t.visualization_frp, &t.frp.network, Some(t.model.outer_dom.clone_ref()))
|
||||
Self::new(
|
||||
&t,
|
||||
&t.visualization_frp,
|
||||
&t.frp.network,
|
||||
Some(t.model.style_container.clone_ref()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -87,9 +87,8 @@ fn database() -> SuggestionDatabase {
|
||||
#[with_doc_section(doc_section!("It also contains the autobiography of the author of \
|
||||
this code."))]
|
||||
#[with_doc_section(doc_section!("And a long list of his cats."))]
|
||||
#[with_doc_section(doc_section!(
|
||||
"Here it is" => "<ul><li>Tom</li><li>Garfield</li><li>Mr. Bigglesworth</li></ul>"
|
||||
))]
|
||||
#[with_doc_section(doc_section!("Here it is" => ""))]
|
||||
#[with_doc_section(doc_section!(- "Tom"; - "Garfield"; - "Mr. Bigglesworth"))]
|
||||
#[with_doc_section(doc_section!(! "Important", "Important sections are used to warn the \
|
||||
reader about the dangers of using the module."))]
|
||||
#[with_doc_section(doc_section!(? "Info", "Info sections provide some insights."))]
|
||||
@ -99,21 +98,22 @@ fn database() -> SuggestionDatabase {
|
||||
type Delimited_Format (a) {
|
||||
#[with_doc_section(doc_section!("Some constructor."))]
|
||||
#[with_doc_section(doc_section!(> "Example", "Some 1"))]
|
||||
#[with_doc_section(doc_section!("Documentation for the Some(a) constructor."))]
|
||||
#[with_doc_section(doc_section!("Documentation for the <code>Some(a)</code> constructor."))]
|
||||
Some (a);
|
||||
#[with_doc_section(doc_section!("Documentation for the None constructor."))]
|
||||
#[with_doc_section(doc_section!("Documentation for the <code>None</code> constructor."))]
|
||||
None;
|
||||
|
||||
#[with_doc_section(doc_section!("Documentation for the is_some() method."))]
|
||||
#[with_doc_section(doc_section!("Arguments" => "<ul><li>self</li></ul>"))]
|
||||
#[with_doc_section(doc_section!("Documentation for the <code>is_some()</code> method."))]
|
||||
#[with_doc_section(doc_section!("Arguments" => ""))]
|
||||
#[with_doc_section(doc_section!(- "self", "Self argument"))]
|
||||
#[with_doc_section(doc_section!(! "Important", "This method is important."))]
|
||||
fn is_some(self) -> Standard.Base.Boolean;
|
||||
|
||||
#[with_doc_section(doc_section!("Documentation for the Maybe.map() method."))]
|
||||
#[with_doc_section(doc_section!("Documentation for the <code>Maybe.map()</code> method."))]
|
||||
fn comment_all_characters (self) -> Standard.Base.Maybe;
|
||||
}
|
||||
|
||||
#[with_doc_section(doc_section!("Documentation for the foo method."))]
|
||||
#[with_doc_section(doc_section!("Documentation for the <code>foo</code> method."))]
|
||||
fn foo(a: Standard.Base.Maybe) -> Standard.Base.Boolean;
|
||||
|
||||
#[with_doc_section(doc_section!(> "Example", "Get the names of all of the items from \
|
||||
@ -135,7 +135,9 @@ fn database() -> SuggestionDatabase {
|
||||
let args = vec![Argument::new("a", "Standard.Base.Boolean")];
|
||||
builder.add_function("bar", args, "Standard.Base.Boolean", scope.clone(), |e| {
|
||||
e.with_doc_sections(vec![
|
||||
DocSection::Paragraph { body: "Documentation for the bar function.".into() },
|
||||
DocSection::Paragraph {
|
||||
body: "Documentation for the <code>bar</code> function.".into(),
|
||||
},
|
||||
DocSection::Tag { tag: Tag::Deprecated, body: default() },
|
||||
DocSection::Marked {
|
||||
mark: Mark::Example,
|
||||
@ -147,7 +149,9 @@ fn database() -> SuggestionDatabase {
|
||||
|
||||
builder.add_local("local1", "Standard.Base.Boolean", scope, |e| {
|
||||
e.with_doc_sections(vec![
|
||||
DocSection::Paragraph { body: "Documentation for the local1 variable.".into() },
|
||||
DocSection::Paragraph {
|
||||
body: "Documentation for the <code>local1</code> variable.".into(),
|
||||
},
|
||||
DocSection::Tag { tag: Tag::Advanced, body: default() },
|
||||
])
|
||||
});
|
||||
@ -273,6 +277,7 @@ pub fn main() {
|
||||
panel_visible <- any(...);
|
||||
panel_visible <+ init.constant(true);
|
||||
current_state <- panel_visible.sample(&show_hide.events_deprecated.mouse_down);
|
||||
panel_visible <+ current_state.not();
|
||||
panel.frp.set_visible <+ current_state.not();
|
||||
|
||||
// === Disable navigator on hover ===
|
||||
|
@ -239,7 +239,12 @@ define_themes! { [light:0, dark:1]
|
||||
width = 406.0, 406.0;
|
||||
height = 380.0, 380.0;
|
||||
background = application::component_browser::component_list_panel::background_color, application::component_browser::component_list_panel::background_color;
|
||||
corner_radius = 14.0, 14.0;
|
||||
corner_radius = 20.0, 20.0;
|
||||
breadcrumbs {
|
||||
padding_y = 8.0, 8.0;
|
||||
padding_x = 10.0, 10.0;
|
||||
height = 40.0, 40.0;
|
||||
}
|
||||
}
|
||||
component_list_panel {
|
||||
width = 190.0, 190.0;
|
||||
|
@ -219,8 +219,11 @@ impl Model {
|
||||
background_height: f32,
|
||||
background_y_offset: f32,
|
||||
) {
|
||||
let background_size = Vector2::new(content_size.x, background_height);
|
||||
self.background.set_size(background_size + Vector2(2.0 * background_padding_x, 0.0));
|
||||
let background_x = content_size.x + 2.0 * background_padding_x;
|
||||
let background_x = background_x.min(size.x);
|
||||
let background_size = Vector2::new(background_x, background_height);
|
||||
|
||||
self.background.set_size(background_size);
|
||||
self.background.set_corner_radius(background_size.y / 2.0);
|
||||
self.background
|
||||
.set_y(-background_height / 2.0 - content_size.y / 2.0 - background_y_offset);
|
||||
|
@ -19,3 +19,4 @@ enso-reflect = { path = "../../reflect" }
|
||||
enso-metamodel = { path = "../../metamodel", features = ["rust"] }
|
||||
enso-metamodel-lexpr = { path = "../../metamodel/lexpr" }
|
||||
lexpr = "0.2.6"
|
||||
pretty_assertions = "1.4"
|
||||
|
@ -62,6 +62,30 @@ impl DocParser {
|
||||
/// Text rendered as HTML (may contain HTML tags).
|
||||
pub type HtmlString = String;
|
||||
|
||||
/// A description of a single argument in the documentation. The name is delimited from the
|
||||
/// description using a colon.
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||
pub struct Argument {
|
||||
/// Name of the argument.
|
||||
pub name: String,
|
||||
/// Description of the argument.
|
||||
pub description: HtmlString,
|
||||
}
|
||||
|
||||
impl Argument {
|
||||
/// Convert the given string to the argument description.
|
||||
pub fn new(text: &str) -> Self {
|
||||
// We split by the first colon or space, whatever comes first.
|
||||
// Typically a colon must be used as a separator, but in some documentation snippets we
|
||||
// have there is no colon and the name of the argument is simply the first word.
|
||||
let split = text.splitn(2, |c| c == ':' || c == ' ');
|
||||
let (name, description) = split.collect_tuple().unwrap_or((text, ""));
|
||||
let name = name.trim().to_string();
|
||||
let description = description.trim().to_string();
|
||||
Self { name, description }
|
||||
}
|
||||
}
|
||||
|
||||
/// A single section of the documentation.
|
||||
#[derive(Hash, Debug, Clone, PartialEq, Eq)]
|
||||
#[allow(missing_docs)]
|
||||
@ -78,6 +102,10 @@ pub enum DocSection {
|
||||
/// The elements that make up this paragraph.
|
||||
body: HtmlString,
|
||||
},
|
||||
/// A list of items. Each item starts with a dash (`-`).
|
||||
List { items: Vec<HtmlString> },
|
||||
/// A list of items, but each item is an [`Argument`]. Starts with `Arguments:` keyword.
|
||||
Arguments { args: Vec<Argument> },
|
||||
/// The section that starts with the key followed by the colon and the body.
|
||||
Keyed {
|
||||
/// The section key.
|
||||
@ -106,7 +134,9 @@ pub enum DocSection {
|
||||
struct DocSectionCollector {
|
||||
sections: Vec<DocSection>,
|
||||
in_secondary_section: bool,
|
||||
inside_arguments: bool,
|
||||
current_body: String,
|
||||
current_list: Vec<String>,
|
||||
}
|
||||
|
||||
impl DocSectionCollector {
|
||||
@ -118,6 +148,7 @@ impl DocSectionCollector {
|
||||
Some(DocSection::Paragraph { body, .. })
|
||||
| Some(DocSection::Keyed { body, .. })
|
||||
| Some(DocSection::Marked { body, .. }) => *body = text,
|
||||
Some(DocSection::List { .. }) | Some(DocSection::Arguments { .. }) => (),
|
||||
Some(DocSection::Tag { .. }) | None =>
|
||||
self.sections.push(DocSection::Paragraph { body: text }),
|
||||
}
|
||||
@ -127,13 +158,16 @@ impl DocSectionCollector {
|
||||
self.finish_section();
|
||||
let result = self.sections.drain(..).collect();
|
||||
let current_body = std::mem::take(&mut self.current_body);
|
||||
let current_list = std::mem::take(&mut self.current_list);
|
||||
let sections = std::mem::take(&mut self.sections);
|
||||
*self = Self {
|
||||
// Reuse the (empty) buffers.
|
||||
current_body,
|
||||
current_list,
|
||||
sections,
|
||||
// Reset the rest of state.
|
||||
in_secondary_section: Default::default(),
|
||||
inside_arguments: Default::default(),
|
||||
};
|
||||
result
|
||||
}
|
||||
@ -155,6 +189,9 @@ impl<L> TokenConsumer<L> for DocSectionCollector {
|
||||
fn enter_keyed_section(&mut self, header: Span<'_, L>) {
|
||||
self.finish_section();
|
||||
let key = header.to_string();
|
||||
if key.eq_ignore_ascii_case("Arguments") {
|
||||
self.inside_arguments = true;
|
||||
}
|
||||
let body = Default::default();
|
||||
self.sections.push(DocSection::Keyed { key, body });
|
||||
}
|
||||
@ -164,12 +201,10 @@ impl<L> TokenConsumer<L> for DocSectionCollector {
|
||||
}
|
||||
|
||||
fn start_list(&mut self) {
|
||||
self.current_body.push_str("<ul>");
|
||||
self.current_list.clear();
|
||||
}
|
||||
|
||||
fn start_list_item(&mut self) {
|
||||
self.current_body.push_str("<li>");
|
||||
}
|
||||
fn start_list_item(&mut self) {}
|
||||
|
||||
fn start_paragraph(&mut self) {
|
||||
let first_content = !self.in_secondary_section && self.current_body.is_empty();
|
||||
@ -203,8 +238,19 @@ impl<L> TokenConsumer<L> for DocSectionCollector {
|
||||
|
||||
fn end(&mut self, scope: ScopeType) {
|
||||
match scope {
|
||||
ScopeType::List => self.current_body.push_str("</ul>"),
|
||||
ScopeType::ListItem => (),
|
||||
ScopeType::List => {
|
||||
let items = mem::take(&mut self.current_list);
|
||||
if self.inside_arguments {
|
||||
let args = items.iter().map(|arg| Argument::new(arg)).collect();
|
||||
self.sections.push(DocSection::Arguments { args });
|
||||
self.inside_arguments = false;
|
||||
} else {
|
||||
self.sections.push(DocSection::List { items })
|
||||
}
|
||||
}
|
||||
ScopeType::ListItem => {
|
||||
self.current_list.push(self.current_body.drain(..).collect());
|
||||
}
|
||||
ScopeType::Paragraph => (),
|
||||
ScopeType::Raw => self.current_body.push_str("</pre>"),
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ use enso_prelude::*;
|
||||
pub mod doc_sections;
|
||||
|
||||
pub use doc_sections::parse;
|
||||
pub use doc_sections::Argument;
|
||||
pub use doc_sections::DocSection;
|
||||
|
||||
|
||||
@ -691,3 +692,70 @@ pub trait TokenConsumer<L> {
|
||||
/// the most recently-opened scope that has not been closed.
|
||||
fn end(&mut self, scope: ScopeType);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_list_parsing() {
|
||||
use crate::doc_sections::Argument;
|
||||
use crate::DocSection::*;
|
||||
use crate::Mark::*;
|
||||
use crate::Tag::*;
|
||||
|
||||
let docs = r#"
|
||||
ALIAS From Text
|
||||
|
||||
Parses a textual representation of an integer into an integer number, returning
|
||||
a `Number_Parse_Error` if the text does not represent a valid integer.
|
||||
|
||||
Arguments:
|
||||
- text: The text to parse into a integer.
|
||||
- radix: The number base to use for parsing (defaults to 10). `radix`
|
||||
must be between 2 and 36 (inclusive)
|
||||
- arg argument without colon
|
||||
- argument_without_description
|
||||
|
||||
- List item 1
|
||||
- List item 2
|
||||
- List item 3
|
||||
|
||||
> Example
|
||||
Parse the text "20220216" into an integer number.
|
||||
|
||||
Integer.parse "20220216""#;
|
||||
let res = parse(docs);
|
||||
let expected = [
|
||||
Tag { tag: Alias, body: "From Text".into() },
|
||||
Paragraph { body: "Parses a textual representation of an integer into an integer number, \
|
||||
returning a <code>Number_Parse_Error</code> if the text does not represent a valid integer.".into() },
|
||||
Keyed { key: "Arguments".into(), body: "".into() },
|
||||
Arguments { args: [
|
||||
Argument {
|
||||
name: "text".into(),
|
||||
description: "The text to parse into a integer.".into() },
|
||||
Argument {
|
||||
name: "radix".into(),
|
||||
description: "The number base to use for parsing (defaults to 10). <code>radix</code> \
|
||||
must be between 2 and 36 (inclusive)".into()
|
||||
},
|
||||
Argument {
|
||||
name: "arg".into(),
|
||||
description: "argument without colon".into()
|
||||
},
|
||||
Argument {
|
||||
name: "argument_without_description".into(),
|
||||
description: default(),
|
||||
}].to_vec()
|
||||
},
|
||||
List { items: ["List item 1".into(), "List item 2".into(), "List item 3".into()].to_vec() },
|
||||
Marked {
|
||||
mark: Example,
|
||||
header: Some("Example".into()),
|
||||
body: "<p>Parse the text \"20220216\" into an integer number.<pre>\nInteger.parse \"20220216\"</pre>".into()
|
||||
}].to_vec();
|
||||
assert_eq!(res, expected);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! Prints a debug representation of Enso documentation found in the given Enso sourec file(s).
|
||||
//! Prints a debug representation of Enso documentation found in the given Enso source file(s).
|
||||
|
||||
#![recursion_limit = "256"]
|
||||
// === Features ===
|
||||
|
Loading…
Reference in New Issue
Block a user