use super::render_context::RenderContext;
use super::types::render_type_def_colon;
use super::types::with_trailing_comma;
use crate::html::DiffStatus;
use crate::html::util::DocEntryCtx;
use crate::html::util::Id;
use crate::html::util::IdBuilder;
use crate::html::util::IdKind;
use crate::html::util::Tag;
use crate::js_doc::JsDoc;
use crate::js_doc::JsDocTag;
use crate::params::ParamDef;
use crate::params::ParamPatternDef;
use indexmap::IndexSet;
use std::ops::Deref;
fn param_doc_name(param: &ParamDef) -> Option<&str> {
match ¶m.pattern {
ParamPatternDef::Identifier { name, .. } => Some(name),
ParamPatternDef::Rest { arg } => param_doc_name(arg),
ParamPatternDef::Assign { left, .. } => param_doc_name(left),
ParamPatternDef::Array { .. } | ParamPatternDef::Object { .. } => None,
}
}
pub(crate) fn render_param_doc_entries(
ctx: &RenderContext,
params: &[ParamDef],
js_doc: &JsDoc,
parent_id: &Id,
location: &crate::Location,
mut diff_info: impl FnMut(
usize,
&ParamDef,
) -> (Option<DiffStatus>, Option<String>),
) -> Vec<DocEntryCtx> {
let param_docs = js_doc
.tags
.iter()
.filter_map(|tag| {
if let JsDocTag::Param {
name,
doc,
optional,
default,
..
} = tag
{
Some((name.deref(), (doc, *optional, default)))
} else {
None
}
})
.collect::<std::collections::HashMap<
&str,
(&Option<Box<str>>, bool, &Option<Box<str>>),
>>();
params
.iter()
.enumerate()
.map(|(i, param)| {
let (name, str_name) = param_name(param, i);
let param_doc = param_doc_name(param).and_then(|n| param_docs.get(n));
let id = IdBuilder::new_with_parent(ctx, parent_id)
.kind(IdKind::Parameter)
.name(&str_name)
.build();
let (mut default, optional) =
if let Some((_doc, optional, default)) = param_doc {
((**default).to_owned(), *optional)
} else {
(None, false)
};
let ts_type =
if let ParamPatternDef::Assign { left, right } = ¶m.pattern {
default = default.or(Some(right.deref().into()));
left.ts_type.as_ref()
} else {
param.ts_type.as_ref()
};
let mut ts_type = ts_type
.map(|ts_type| render_type_def_colon(ctx, ts_type))
.unwrap_or_default();
if let Some(default) = &default
&& default.deref() != "[UNSUPPORTED]" {
ts_type = format!(r#"{ts_type}<span><span class="font-normal"> = </span>{default}</span>"#);
}
let tags = if matches!(
param.pattern,
ParamPatternDef::Array { optional, .. }
| ParamPatternDef::Identifier { optional, .. }
| ParamPatternDef::Object { optional, .. }
if optional
) || default.is_some()
|| optional
{
IndexSet::from([Tag::Optional])
} else {
IndexSet::new()
};
let param_doc = param_doc.and_then(|(doc, _, _)| doc.as_deref());
let (diff_status, old_content) = diff_info(i, param);
DocEntryCtx::new(
ctx,
id,
Some(name),
None,
&ts_type,
tags,
param_doc,
location,
diff_status,
old_content,
None,
None,
)
})
.collect()
}
pub(crate) fn attach_signature_docs(
ctx: &RenderContext,
entry: &mut DocEntryCtx,
params: &[ParamDef],
js_doc: &JsDoc,
parent_id: &Id,
location: &crate::Location,
) {
let has_param_docs = js_doc
.tags
.iter()
.any(|tag| matches!(tag, JsDocTag::Param { doc, .. } if doc.is_some()));
if has_param_docs {
entry.params = render_param_doc_entries(
ctx,
params,
js_doc,
parent_id,
location,
|_, _| (None, None),
);
}
entry.return_doc = js_doc.tags.iter().find_map(|tag| {
if let JsDocTag::Return { doc, .. } = tag {
doc
.as_deref()
.map(|doc| crate::html::jsdoc::render_markdown(ctx, doc, true))
} else {
None
}
});
}
pub(crate) fn render_params(
ctx: &RenderContext,
params: &[ParamDef],
) -> String {
if params.is_empty() {
String::new()
} else if params.len() == 1 {
format!("<span>{}</span>", render_param(ctx, ¶ms[0], 0))
} else {
let last = params.len() - 1;
let mut items = Vec::with_capacity(params.len());
for (i, def) in params.iter().enumerate() {
let rendered = render_param(ctx, def, i);
let content = if i < last {
with_trailing_comma(&rendered)
} else {
rendered
};
items.push(format!("<div>{content}</div>"));
}
let content = items.join("");
format!(r#"<div class="ml-4">{content}</div>"#)
}
}
fn render_param(ctx: &RenderContext, param: &ParamDef, i: usize) -> String {
let (name, _str_name) = param_name(param, i);
let ts_type = if let ParamPatternDef::Assign { left, .. } = ¶m.pattern {
left.ts_type.as_ref().or(param.ts_type.as_ref())
} else {
param.ts_type.as_ref()
};
let ts_type = ts_type
.map(|ts_type| render_type_def_colon(ctx, ts_type))
.unwrap_or_default();
let question_mark = match param.pattern {
ParamPatternDef::Array { optional, .. } if optional => {
r#"<span class="td-op">?</span>"#
}
ParamPatternDef::Assign { .. } => r#"<span class="td-op">?</span>"#,
ParamPatternDef::Identifier { optional, .. } if optional => {
r#"<span class="td-op">?</span>"#
}
ParamPatternDef::Object { optional, .. } if optional => {
r#"<span class="td-op">?</span>"#
}
_ => "",
};
format!("<span>{name}{question_mark}{ts_type}</span>")
}
pub(crate) fn param_name(param: &ParamDef, i: usize) -> (String, String) {
match ¶m.pattern {
ParamPatternDef::Array { .. } | ParamPatternDef::Object { .. } => (
format!(r#"<span class="italic">arg_{i}</span>"#),
format!("arg_{i}"),
),
ParamPatternDef::Assign { left, .. } => param_name(left, i),
ParamPatternDef::Identifier { name, .. } => {
(html_escape::encode_text(name).into_owned(), name.clone())
}
ParamPatternDef::Rest { arg } => (
format!("<span>...{}</span>", param_name(arg, i).0),
format!("...{}", param_name(arg, i).1),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn destructured(pattern: ParamPatternDef) -> ParamDef {
ParamDef {
pattern,
decorators: Box::new([]),
ts_type: None,
}
}
#[test]
fn unnamed_object_param_is_named_by_index() {
let param = destructured(ParamPatternDef::Object {
props: vec![],
optional: false,
});
let (html, str_name) = param_name(¶m, 0);
assert_eq!(html, r#"<span class="italic">arg_0</span>"#);
assert_eq!(str_name, "arg_0");
}
#[test]
fn unnamed_array_param_is_named_by_index() {
let param = destructured(ParamPatternDef::Array {
elements: vec![],
optional: false,
});
let (html, str_name) = param_name(¶m, 2);
assert_eq!(html, r#"<span class="italic">arg_2</span>"#);
assert_eq!(str_name, "arg_2");
}
}