use std::fmt::Write;
use std::iter;
use itertools::Itertools;
use crate::docs::DocFunction;
use crate::docs::DocItem;
use crate::docs::DocMember;
use crate::docs::DocModule;
use crate::docs::DocParam;
use crate::docs::DocProperty;
use crate::docs::DocString;
use crate::docs::DocType;
use crate::typing::ty::TypeRenderConfig;
use crate::typing::Ty;
enum DSOpts {
Summary,
Details,
Combined,
}
fn render_doc_string(opts: DSOpts, string: &Option<DocString>) -> Option<String> {
string.as_ref().and_then(|d| match opts {
DSOpts::Summary => Some(d.summary.clone()),
DSOpts::Details => d.details.clone(),
DSOpts::Combined => Some(match &d.details {
Some(details) => format!("{}\n\n{}", d.summary, details),
None => d.summary.clone(),
}),
})
}
fn escape_name(name: &str) -> String {
name.replace('_', "\\_")
}
fn render_property(name: &str, property: &DocProperty, render_config: &TypeRenderConfig) -> String {
let prototype = render_code_block(
&format!("{name}: {}", &property.typ.display_with(render_config)),
render_config,
);
let header = format!("## {}\n\n{prototype}", escape_name(name));
let summary = render_doc_string(DSOpts::Summary, &property.docs);
let details = render_doc_string(DSOpts::Details, &property.docs);
let mut body = header;
if let Some(summary) = summary {
body.push_str("\n\n");
body.push_str(&summary);
}
if let Some(details) = details {
body.push_str("\n\n");
body.push_str(&details);
}
body
}
fn render_function_parameters<'a>(
params: impl IntoIterator<Item = (String, &'a DocParam)>,
) -> Option<String> {
let mut param_list: Option<String> = None;
for (name, p) in params {
let DocParam { docs, .. } = p;
if docs.is_none() {
continue;
}
let param_list = param_list.get_or_insert_with(String::new);
let docs = render_doc_string(DSOpts::Combined, docs).unwrap_or_default();
let mut lines_iter = docs.lines();
if let Some(first_line) = lines_iter.next() {
let _ = writeln!(param_list, "* `{name}`: {first_line}");
for line in lines_iter {
let _ = writeln!(param_list, " {line}");
}
} else {
let _ = writeln!(param_list, "* `{name}`");
}
}
param_list
}
fn render_function(
name: &str,
function: &DocFunction,
include_header: bool,
render_config: &TypeRenderConfig,
) -> String {
let prototype = render_code_block(
&render_function_prototype(name, function, render_config),
render_config,
);
let header = if include_header {
format!("## {}\n\n{prototype}", escape_name(name))
} else {
prototype
};
let summary = render_doc_string(DSOpts::Summary, &function.docs);
let details = render_doc_string(DSOpts::Details, &function.docs);
let parameter_docs =
render_function_parameters(function.params.doc_params_with_starred_names());
let return_docs = render_doc_string(DSOpts::Combined, &function.ret.docs);
let mut body = header;
if let Some(summary) = &summary {
body.push_str("\n\n");
body.push_str(summary);
}
if let Some(parameter_docs) = ¶meter_docs {
body.push_str("\n\n#### Parameters\n\n");
body.push_str(parameter_docs);
}
if let Some(returns) = &return_docs {
body.push_str("\n\n#### Returns\n\n");
body.push_str(returns);
}
if let Some(details) = &details {
if parameter_docs.is_some() || return_docs.is_some() {
body.push_str("\n\n#### Details\n\n");
} else {
body.push_str("\n\n");
}
body.push_str(details);
}
body
}
pub(super) fn render_members<'a>(
name: &str,
docs: &Option<DocString>,
prefix: &str,
members: impl IntoIterator<Item = (&'a str, DocMember)>,
after_summary: Option<String>,
render_config: &TypeRenderConfig,
) -> String {
let summary = render_doc_string(DSOpts::Combined, docs)
.map(|s| format!("\n\n{}", s))
.unwrap_or_default();
let member_details = members
.into_iter()
.sorted_by(|(l_m, _), (r_m, _)| l_m.cmp(r_m))
.map(|(child, member)| {
render_doc_member(&format!("{prefix}{child}"), &member, render_config)
});
let member_details: Vec<_> = after_summary.into_iter().chain(member_details).collect();
let members_details = member_details.join("\n\n---\n\n");
format!("# {name}{summary}\n\n{members_details}")
}
pub(super) fn render_doc_type(
name: &str,
prefix: &str,
t: &DocType,
render_config: &TypeRenderConfig,
) -> String {
let constructor = t
.constructor
.as_ref()
.map(|c| render_function(name, c, false, render_config));
render_members(
&name,
&t.docs,
&prefix,
t.members.iter().map(|(n, m)| (&**n, m.clone())),
constructor,
render_config,
)
}
pub fn render_doc_item_no_link(name: &str, item: &DocItem) -> String {
render_doc_item(name, item, &TypeRenderConfig::Default)
}
pub fn render_doc_item(name: &str, item: &DocItem, render_config: &TypeRenderConfig) -> String {
match item {
DocItem::Module(m) => render_members(
name,
&m.docs,
"",
m.members.iter().filter_map(|(n, m)| {
m.try_as_member_with_collapsed_object()
.ok()
.map(|m| (&**n, m))
}),
None,
render_config,
),
DocItem::Type(o) => render_doc_type(
&format!("`{name}` type"),
&format!("{name}."),
o,
render_config,
),
DocItem::Member(DocMember::Function(f)) => render_function(name, f, true, render_config),
DocItem::Member(DocMember::Property(p)) => render_property(name, p, render_config),
}
}
pub fn render_doc_member(name: &str, item: &DocMember, render_config: &TypeRenderConfig) -> String {
match item {
DocMember::Function(f) => render_function(name, f, true, render_config),
DocMember::Property(p) => render_property(name, p, render_config),
}
}
pub fn render_doc_param(starred_name: String, item: &DocParam) -> String {
render_function_parameters(iter::once((starred_name, item))).unwrap_or_default()
}
const MAX_ARGS_BEFORE_MULTILINE: usize = 3;
const MAX_LENGTH_BEFORE_MULTILINE: usize = 80;
fn raw_type_prefix(prefix: &str, t: &Ty, render_config: &TypeRenderConfig) -> String {
if t.is_any() {
String::new()
} else {
format!("{prefix}{}", t.display_with(render_config))
}
}
fn render_function_prototype(
function_name: &str,
f: &DocFunction,
render_config: &TypeRenderConfig,
) -> String {
let ret_type = raw_type_prefix(" -> ", &f.ret.typ, render_config);
let prefix = format!("def {}", function_name);
let one_line_params = f.params.render_code(None, render_config);
let single_line_result = format!("{}({}){}", prefix, one_line_params, ret_type);
if f.params.doc_params().count() > MAX_ARGS_BEFORE_MULTILINE
|| single_line_result.len() > MAX_LENGTH_BEFORE_MULTILINE
{
let chunked_params = f.params.render_code(Some(" "), render_config);
format!("{}(\n{}){}", prefix, chunked_params, ret_type)
} else {
single_line_result
}
}
fn render_code_block(contents: &str, render_config: &TypeRenderConfig) -> String {
match render_config {
TypeRenderConfig::Default => format!("```python\n{contents}\n```"),
TypeRenderConfig::LinkedType {
render_linked_ty_starlark_value: _,
} => {
format!(r#"<pre class="language-python"><code>{contents}</code></pre>"#)
}
}
}
impl DocModule {
pub(super) fn render_markdown_page_for_multipage_render(
&self,
name: &str,
render_config: &TypeRenderConfig,
) -> String {
render_members(
name,
&self.docs,
"",
self.members
.iter()
.filter_map(|(n, m)| m.try_as_member().map(|m| (&**n, m))),
None,
render_config,
)
}
}
impl DocType {
pub(super) fn render_markdown_page_for_multipage_render(
&self,
name: &str,
render_config: &TypeRenderConfig,
) -> String {
render_doc_type(&name, &format!("{name}."), self, render_config)
}
}