use crate::Symbol;
use crate::node::DeclarationDef;
use deno_ast::ModuleSpecifier;
use handlebars::Handlebars;
use handlebars::handlebars_helper;
use indexmap::IndexMap;
use indexmap::IndexSet;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
pub mod diff;
pub mod jsdoc;
pub mod pages;
mod parameters;
pub mod partition;
mod render_context;
pub mod search;
mod symbols;
mod types;
mod usage;
pub mod util;
#[cfg(feature = "comrak")]
pub mod comrak;
use crate::html::pages::SymbolPage;
use crate::js_doc::JsDocTag;
use crate::parser::ParseOutput;
pub use diff::DiffIndex;
pub use diff::DiffStatus;
pub use pages::generate_symbol_pages_for_module;
pub use render_context::RenderContext;
pub use search::generate_search_index;
pub use symbols::AllSymbolsCtx;
pub use symbols::AllSymbolsEntrypointCtx;
pub use symbols::SymbolContentCtx;
pub use symbols::SymbolGroupCtx;
pub use symbols::namespace;
pub use usage::UsageComposer;
pub use usage::UsageComposerEntry;
pub use usage::UsageToMd;
pub use util::DocNodeKindCtx;
pub use util::HrefResolver;
pub use util::NamespacedGlobalSymbols;
pub use util::SectionHeaderCtx;
pub use util::ToCCtx;
pub use util::TopSymbolCtx;
pub use util::TopSymbolsCtx;
pub use util::UrlResolveKind;
pub use util::compute_namespaced_symbols;
pub use util::href_path_resolve;
pub use util::qualify_drilldown_name;
pub const STYLESHEET: &str = include_str!("./templates/styles.gen.css");
pub const STYLESHEET_FILENAME: &str = "styles.css";
pub const PAGE_STYLESHEET: &str =
include_str!("./templates/pages/page.gen.css");
pub const PAGE_STYLESHEET_FILENAME: &str = "page.css";
pub const RESET_STYLESHEET: &str =
include_str!("./templates/pages/reset.gen.css");
pub const RESET_STYLESHEET_FILENAME: &str = "reset.css";
const SEARCH_INDEX_FILENAME: &str = "search_index.js";
pub const SCRIPT_JS: &str = include_str!("./templates/script.js");
pub const SCRIPT_FILENAME: &str = "script.js";
const FUSE_JS: &str = include_str!("./templates/pages/fuse.js");
const FUSE_FILENAME: &str = "fuse.js";
const SEARCH_JS: &str = include_str!("./templates/pages/search.js");
const SEARCH_FILENAME: &str = "search.js";
const DARKMODE_TOGGLE_JS: &str =
include_str!("./templates/pages/darkmode_toggle.js");
const DARKMODE_TOGGLE_FILENAME: &str = "darkmode_toggle.js";
fn setup_hbs() -> Result<Handlebars<'static>, anyhow::Error> {
let mut reg = Handlebars::new();
reg.register_escape_fn(|str| html_escape::encode_safe(str).into_owned());
reg.set_strict_mode(true);
#[cfg(debug_assertions)]
reg.set_dev_mode(true);
handlebars_helper!(concat: |a: str, b: str| format!("{a}{b}"));
reg.register_helper("concat", Box::new(concat));
handlebars_helper!(print: |a: Json| println!("{a:#?}"));
reg.register_helper("print", Box::new(print));
reg.register_template_string(
ToCCtx::TEMPLATE,
include_str!("./templates/toc.hbs"),
)?;
reg.register_template_string(
util::DocEntryCtx::TEMPLATE,
include_str!("./templates/doc_entry.hbs"),
)?;
reg.register_template_string(
util::SectionCtx::TEMPLATE,
include_str!("./templates/section.hbs"),
)?;
reg.register_template_string(
"doc_node_kind_icon",
include_str!("./templates/doc_node_kind_icon.hbs"),
)?;
reg.register_template_string(
"namespace_section",
include_str!("./templates/namespace_section.hbs"),
)?;
reg.register_template_string(
symbols::DocBlockSubtitleCtx::TEMPLATE_CLASS,
include_str!("./templates/doc_block_subtitle_class.hbs"),
)?;
reg.register_template_string(
symbols::DocBlockSubtitleCtx::TEMPLATE_INTERFACE,
include_str!("./templates/doc_block_subtitle_interface.hbs"),
)?;
reg.register_template_string(
util::AnchorCtx::TEMPLATE,
include_str!("./templates/anchor.hbs"),
)?;
reg.register_template_string(
SymbolGroupCtx::TEMPLATE,
include_str!("./templates/symbol_group.hbs"),
)?;
reg.register_template_string(
SymbolContentCtx::TEMPLATE,
include_str!("./templates/symbol_content.hbs"),
)?;
reg.register_template_string(
jsdoc::ExampleCtx::TEMPLATE,
include_str!("./templates/example.hbs"),
)?;
reg.register_template_string(
symbols::function::FunctionCtx::TEMPLATE,
include_str!("./templates/function.hbs"),
)?;
reg.register_template_string(
jsdoc::ModuleDocCtx::TEMPLATE,
include_str!("./templates/module_doc.hbs"),
)?;
reg.register_template_string(
util::BreadcrumbsCtx::TEMPLATE,
include_str!("./templates/breadcrumbs.hbs"),
)?;
reg.register_template_string(
usage::UsagesCtx::TEMPLATE,
include_str!("./templates/usages.hbs"),
)?;
reg.register_template_string(
"usages_large",
include_str!("./templates/usages_large.hbs"),
)?;
reg.register_template_string(
util::Tag::TEMPLATE,
include_str!("./templates/tag.hbs"),
)?;
reg.register_template_string(
"source_button",
include_str!("./templates/source_button.hbs"),
)?;
reg.register_template_string(
"deprecated",
include_str!("./templates/deprecated.hbs"),
)?;
reg.register_template_string(
"index_signature",
include_str!("./templates/index_signature.hbs"),
)?;
reg.register_template_string(
pages::CategoriesPanelCtx::TEMPLATE,
include_str!("./templates/category_panel.hbs"),
)?;
reg.register_template_string("see", include_str!("./templates/see.hbs"))?;
reg.register_template_string(
AllSymbolsCtx::TEMPLATE,
include_str!("./templates/all_symbols.hbs"),
)?;
reg.register_template_string(
pages::HtmlHeadCtx::TEMPLATE,
include_str!("./templates/pages/html_head.hbs"),
)?;
reg.register_template_string(
pages::AllSymbolsPageCtx::TEMPLATE,
include_str!("./templates/pages/all_symbols.hbs"),
)?;
reg.register_template_string(
pages::SymbolPageCtx::TEMPLATE,
include_str!("./templates/pages/symbol.hbs"),
)?;
reg.register_template_string(
pages::IndexCtx::TEMPLATE,
include_str!("./templates/pages/index.hbs"),
)?;
reg.register_template_string(
"pages/top_nav",
include_str!("./templates/pages/top_nav.hbs"),
)?;
reg.register_template_string(
"pages/search_results",
include_str!("./templates/pages/search_results.hbs"),
)?;
reg.register_template_string(
"pages/redirect",
include_str!("./templates/pages/redirect.hbs"),
)?;
reg.register_template_string(
"icons/arrow",
include_str!("./templates/icons/arrow.svg"),
)?;
reg.register_template_string(
"icons/copy",
include_str!("./templates/icons/copy.svg"),
)?;
reg.register_template_string(
"icons/check",
include_str!("./templates/icons/check.svg"),
)?;
reg.register_template_string(
"icons/link",
include_str!("./templates/icons/link.svg"),
)?;
reg.register_template_string(
"icons/source",
include_str!("./templates/icons/source.svg"),
)?;
reg.register_template_string(
"icons/menu",
include_str!("./templates/icons/menu.svg"),
)?;
reg.register_template_string(
"icons/sun",
include_str!("./templates/icons/sun.svg"),
)?;
reg.register_template_string(
"icons/moon",
include_str!("./templates/icons/moon.svg"),
)?;
Ok(reg)
}
lazy_static! {
pub static ref HANDLEBARS: Handlebars<'static> = setup_hbs().unwrap();
}
pub type HeadInject = Arc<dyn Fn(&str) -> String + Send + Sync>;
#[derive(Clone)]
pub struct GenerateOptions {
pub package_name: Option<String>,
pub main_entrypoint: Option<ModuleSpecifier>,
pub href_resolver: Arc<dyn HrefResolver>,
pub usage_composer: Option<Arc<dyn UsageComposer>>,
pub rewrite_map: Option<IndexMap<ModuleSpecifier, String>>,
pub category_docs: Option<IndexMap<String, Option<String>>>,
pub disable_search: bool,
pub symbol_redirect_map: Option<IndexMap<String, IndexMap<String, String>>>,
pub default_symbol_map: Option<IndexMap<String, String>>,
pub markdown_renderer: jsdoc::MarkdownRenderer,
pub markdown_stripper: jsdoc::MarkdownStripper,
pub head_inject: Option<HeadInject>,
pub id_prefix: Option<String>,
pub diff_only: bool,
}
#[non_exhaustive]
pub struct GenerateCtx {
pub package_name: Option<String>,
pub common_ancestor: Option<PathBuf>,
pub module_docs: IndexMap<Arc<ShortPath>, crate::js_doc::JsDoc>,
pub imports: IndexMap<Arc<ShortPath>, Vec<crate::node::Import>>,
pub doc_nodes: IndexMap<Arc<ShortPath>, Vec<DocNodeWithContext>>,
pub href_resolver: Arc<dyn HrefResolver>,
pub usage_composer: Option<Arc<dyn UsageComposer>>,
pub rewrite_map: Option<IndexMap<ModuleSpecifier, String>>,
pub main_entrypoint: Option<Arc<ShortPath>>,
pub file_mode: FileMode,
pub category_docs: Option<IndexMap<String, Option<String>>>,
pub disable_search: bool,
pub symbol_redirect_map: Option<IndexMap<String, IndexMap<String, String>>>,
pub default_symbol_map: Option<IndexMap<String, String>>,
pub markdown_renderer: jsdoc::MarkdownRenderer,
pub markdown_stripper: jsdoc::MarkdownStripper,
pub head_inject: Option<HeadInject>,
pub id_prefix: Option<String>,
pub diff_only: bool,
reference_index: std::sync::OnceLock<
HashMap<crate::Location, Vec<(usize, DocNodeWithContext)>>,
>,
pub diff: Option<DiffIndex>,
}
impl GenerateCtx {
pub fn new(
options: GenerateOptions,
common_ancestor: Option<PathBuf>,
file_mode: FileMode,
doc_nodes_by_url: ParseOutput,
diff: Option<crate::diff::DocDiff>,
) -> Result<Self, anyhow::Error> {
let diff = diff.map(DiffIndex::new);
let mut main_entrypoint = None;
let mut module_docs = IndexMap::new();
let mut imports = IndexMap::new();
let mut doc_nodes = doc_nodes_by_url
.into_iter()
.map(|(specifier, document)| {
let short_path = Arc::new(ShortPath::new(
specifier,
options.main_entrypoint.as_ref(),
options.rewrite_map.as_ref(),
common_ancestor.as_ref(),
));
if short_path.is_main {
main_entrypoint = Some(short_path.clone());
}
module_docs.insert(short_path.clone(), document.module_doc);
imports.insert(short_path.clone(), document.imports);
let nodes = document
.symbols
.into_iter()
.map(|mut symbol| {
if &*symbol.name == "default"
&& let Some(default_rename) =
options.default_symbol_map.as_ref().and_then(
|default_symbol_map| default_symbol_map.get(&short_path.path),
)
{
Arc::make_mut(&mut symbol).name = default_rename.as_str().into();
}
{
let needs_mutation = symbol.declarations.iter().any(|decl| {
decl
.variable_def()
.as_ref()
.and_then(|def| def.ts_type.as_ref())
.is_some_and(|ts_type| {
matches!(
ts_type.kind,
crate::ts_type::TsTypeDefKind::FnOrConstructor(_)
)
})
});
if needs_mutation {
for declaration in &mut Arc::make_mut(&mut symbol).declarations
{
if let Some(crate::ts_type::TsTypeDefKind::FnOrConstructor(
fn_or_constructor,
)) = declaration
.variable_def()
.as_ref()
.and_then(|def| def.ts_type.as_ref())
.map(|ts_type| ts_type.kind.clone())
{
declaration.def =
DeclarationDef::Function(crate::function::FunctionDef {
def_name: None,
params: fn_or_constructor.params,
return_type: Some(fn_or_constructor.ts_type),
has_body: false,
is_async: false,
is_generator: false,
type_params: fn_or_constructor.type_params,
decorators: Box::new([]),
});
}
}
}
}
let diff_status = diff.as_ref().and_then(|d| {
d.get_symbol_diff(&short_path.specifier, &symbol.name)
.map(|info| info.status.clone())
});
DocNodeWithContext {
origin: short_path.clone(),
ns_qualifiers: Arc::new([]),
inner: symbol,
drilldown_name: None,
drilldown_kind: None,
parent: None,
namespace_children: None,
qualified_name: std::sync::OnceLock::new(),
diff_status,
}
})
.map(|mut node| {
fn handle_node(node: &mut DocNodeWithContext) {
let children = if let Some(ns) = node
.declarations
.iter()
.find_map(|decl| decl.namespace_def())
{
let subqualifier: Arc<[String]> = node.sub_qualifier().into();
Some(
ns.elements
.iter()
.map(|subnode| {
let mut child_node = node.create_namespace_child(
subnode.clone(),
subqualifier.clone(),
);
handle_node(&mut child_node);
child_node
})
.collect::<Vec<_>>(),
)
} else {
None
};
node.namespace_children = children.map(Arc::new);
}
handle_node(&mut node);
node
})
.collect::<Vec<_>>();
(short_path, nodes)
})
.collect::<IndexMap<_, _>>();
doc_nodes.sort_by_key(|a, _| !a.is_main);
if let Some(diff) = &diff {
for (short_path, nodes) in &mut doc_nodes {
if let Some(removed) = diff
.module_diffs
.get(&short_path.specifier)
.map(|diff| &diff.removed)
{
for node in removed {
nodes.push(DocNodeWithContext {
origin: short_path.clone(),
ns_qualifiers: Arc::new([]),
inner: Arc::new(node.clone()),
drilldown_name: None,
drilldown_kind: None,
parent: None,
namespace_children: None,
qualified_name: std::sync::OnceLock::new(),
diff_status: Some(DiffStatus::Removed),
});
}
}
}
for (short_path, symbols) in &mut doc_nodes {
for symbol in symbols.iter_mut() {
let Some(decl) = symbol
.inner
.declarations
.iter()
.find(|decl| decl.namespace_def().is_some())
else {
continue;
};
let ns_diff = diff
.get_declaration_diff(
&short_path.specifier,
&symbol.name,
decl.def.to_kind(),
)
.and_then(|node_diff| node_diff.def_changes.as_ref())
.and_then(|def_diff| {
if let crate::diff::DeclarationDefDiff::Namespace(ns_diff) =
def_diff
{
Some(ns_diff)
} else {
None
}
});
if let Some(ns_diff) = ns_diff {
apply_namespace_diff_inner(symbol, ns_diff, short_path)
}
}
}
}
Ok(Self {
package_name: options.package_name,
common_ancestor,
module_docs,
imports,
doc_nodes,
href_resolver: options.href_resolver,
usage_composer: options.usage_composer,
rewrite_map: options.rewrite_map,
main_entrypoint,
file_mode,
category_docs: options.category_docs,
disable_search: options.disable_search,
symbol_redirect_map: options.symbol_redirect_map,
default_symbol_map: options.default_symbol_map,
markdown_renderer: options.markdown_renderer,
markdown_stripper: options.markdown_stripper,
head_inject: options.head_inject,
id_prefix: options.id_prefix,
diff_only: options.diff_only,
reference_index: std::sync::OnceLock::new(),
diff,
})
}
pub fn create_basic(
mut options: GenerateOptions,
doc_nodes_by_url: ParseOutput,
diff: Option<crate::diff::DocDiff>,
) -> Result<Self, anyhow::Error> {
if doc_nodes_by_url.len() == 1 && options.main_entrypoint.is_none() {
options.main_entrypoint =
Some(doc_nodes_by_url.keys().next().unwrap().clone());
}
let file_mode = match (
doc_nodes_by_url
.keys()
.all(|specifier| specifier.as_str().ends_with(".d.ts")),
doc_nodes_by_url.len(),
) {
(false, 1) => FileMode::Single,
(false, _) => FileMode::Normal,
(true, 1) => FileMode::SingleDts,
(true, _) => FileMode::Dts,
};
let common_ancestor = find_common_ancestor(doc_nodes_by_url.keys(), true);
GenerateCtx::new(
options,
common_ancestor,
file_mode,
doc_nodes_by_url,
diff,
)
}
pub fn render<T: serde::Serialize>(
&self,
template: &str,
data: &T,
) -> String {
HANDLEBARS.render(template, data).unwrap()
}
pub fn resolve_path(
&self,
current: UrlResolveKind,
target: UrlResolveKind,
) -> String {
if let Some(symbol_redirect_map) = &self.symbol_redirect_map
&& let UrlResolveKind::Symbol { file, symbol } = target
&& let Some(path_map) = symbol_redirect_map.get(&file.path)
&& let Some(href) = path_map.get(symbol)
{
return href.clone();
}
self.href_resolver.resolve_path(current, target)
}
fn get_reference_index(
&self,
) -> &HashMap<crate::Location, Vec<(usize, DocNodeWithContext)>> {
self.reference_index.get_or_init(|| {
let mut index: HashMap<
crate::Location,
Vec<(usize, DocNodeWithContext)>,
> = HashMap::new();
fn index_node(
index: &mut HashMap<crate::Location, Vec<(usize, DocNodeWithContext)>>,
node: &DocNodeWithContext,
depth: usize,
) {
for decl in &node.declarations {
index
.entry(decl.location.clone())
.or_default()
.push((depth, node.clone()));
if matches!(decl.def, DeclarationDef::Namespace(..))
&& let Some(children) = &node.namespace_children
{
for child in children.iter() {
index_node(index, child, depth + 1);
}
}
}
}
for nodes in self.doc_nodes.values() {
for node in nodes {
index_node(&mut index, node, 0);
}
}
index
})
}
pub fn resolve_reference<'a>(
&'a self,
new_parent: Option<&'a DocNodeWithContext>,
reference: &'a crate::Location,
) -> impl Iterator<Item = Cow<'a, DocNodeWithContext>> + 'a {
fn strip_qualifiers(node: &mut DocNodeWithContext, depth: usize) {
let ns_qualifiers = node.ns_qualifiers.to_vec();
node.ns_qualifiers = ns_qualifiers[depth..].to_vec().into();
node.qualified_name = std::sync::OnceLock::new();
if let Some(children_rc) = &mut node.namespace_children {
for child in Arc::make_mut(children_rc) {
strip_qualifiers(child, depth);
}
}
}
let entries = self
.get_reference_index()
.get(reference)
.map(|v| v.as_slice())
.unwrap_or(&[]);
entries
.iter()
.map(|(depth, node)| {
if *depth > 0 {
let mut node = node.clone();
strip_qualifiers(&mut node, *depth);
Cow::Owned(node)
} else {
Cow::Borrowed(node)
}
})
.map(move |node| {
if let Some(parent) = new_parent {
let mut node = node.into_owned();
let mut ns_qualifiers = Vec::with_capacity(
parent.ns_qualifiers.len() + node.ns_qualifiers.len(),
);
ns_qualifiers.extend(parent.sub_qualifier());
fn handle_node(
node: &mut DocNodeWithContext,
ns_qualifiers: Vec<String>,
) {
if let Some(children_rc) = &mut node.namespace_children {
for node in Arc::make_mut(children_rc) {
handle_node(node, ns_qualifiers.clone());
}
}
let mut new_ns_qualifiers = ns_qualifiers;
new_ns_qualifiers.extend(node.ns_qualifiers.iter().cloned());
node.ns_qualifiers = new_ns_qualifiers.into();
node.qualified_name = std::sync::OnceLock::new();
}
handle_node(&mut node, ns_qualifiers);
Cow::Owned(node)
} else {
node
}
})
}
}
fn apply_namespace_diff_inner(
node: &mut DocNodeWithContext,
ns_diff: &crate::diff::NamespaceDiff,
short_path: &Arc<ShortPath>,
) {
let removed_ctx = if !ns_diff.removed_elements.is_empty() {
let subqualifier: Arc<[String]> = node.sub_qualifier().into();
let node_snapshot = Arc::new(node.clone());
Some((subqualifier, node_snapshot))
} else {
None
};
if let Some(children_rc) = &mut node.namespace_children {
let children = Arc::make_mut(children_rc);
let added_names: std::collections::HashSet<String> = ns_diff
.added_elements
.iter()
.map(|n| n.name.to_string())
.collect();
let modified_map: IndexMap<String, &crate::diff::SymbolDiff> = ns_diff
.modified_elements
.iter()
.map(|d| (d.name.to_string(), d))
.collect();
for child in children.iter_mut() {
let name = child.name.to_string();
if added_names.contains(&name) {
child.diff_status = Some(DiffStatus::Added);
} else if let Some(symbol_diff) = modified_map.get(&name) {
child.diff_status = if let Some(name_change) = &symbol_diff.name_change
{
Some(DiffStatus::Renamed {
old_name: name_change.old.to_string(),
})
} else {
Some(DiffStatus::Modified)
};
if child
.declarations
.iter()
.any(|decl| matches!(decl.def, DeclarationDef::Namespace(..)))
{
let child_ns_diff =
symbol_diff.declarations.as_ref().and_then(|decls_diff| {
decls_diff.modified.iter().find_map(|decl_diff| {
decl_diff.def_changes.as_ref().and_then(|def_diff| {
if let crate::diff::DeclarationDefDiff::Namespace(ns_diff) =
def_diff
{
Some(ns_diff)
} else {
None
}
})
})
});
if let Some(child_ns_diff) = child_ns_diff {
apply_namespace_diff_inner(child, child_ns_diff, short_path);
}
}
}
}
if let Some((subqualifier, node_snapshot)) = &removed_ctx {
for removed_node in &ns_diff.removed_elements {
children.push(DocNodeWithContext {
origin: short_path.clone(),
ns_qualifiers: subqualifier.clone(),
inner: removed_node.clone(),
drilldown_name: None,
drilldown_kind: None,
parent: Some(node_snapshot.clone()),
namespace_children: None,
qualified_name: std::sync::OnceLock::new(),
diff_status: Some(DiffStatus::Removed),
});
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShortPath {
pub path: String,
pub specifier: ModuleSpecifier,
pub is_main: bool,
}
impl ShortPath {
pub fn new(
specifier: ModuleSpecifier,
main_entrypoint: Option<&ModuleSpecifier>,
rewrite_map: Option<&IndexMap<ModuleSpecifier, String>>,
common_ancestor: Option<&PathBuf>,
) -> Self {
let is_main = main_entrypoint
.is_some_and(|main_entrypoint| main_entrypoint == &specifier);
if let Some(rewrite) =
rewrite_map.and_then(|rewrite_map| rewrite_map.get(&specifier))
{
return ShortPath {
path: rewrite
.strip_prefix('.')
.unwrap_or(rewrite)
.strip_prefix('/')
.unwrap_or(rewrite)
.to_owned(),
specifier,
is_main,
};
}
let Ok(url_file_path) = deno_path_util::url_to_file_path(&specifier) else {
return ShortPath {
path: specifier.to_string(),
specifier,
is_main,
};
};
let stripped_path = common_ancestor
.and_then(|ancestor| url_file_path.strip_prefix(ancestor).ok())
.unwrap_or(&url_file_path);
let path = stripped_path.to_string_lossy().to_string();
let path = path.strip_prefix('/').unwrap_or(&path).to_string();
ShortPath {
path: if path.is_empty() {
".".to_string()
} else {
path
},
specifier,
is_main,
}
}
pub fn display_name(&self) -> &str {
if self.is_main {
"default"
} else {
self
.path
.strip_prefix('.')
.unwrap_or(&self.path)
.strip_prefix('/')
.unwrap_or(&self.path)
}
}
pub fn as_resolve_kind(&self) -> UrlResolveKind<'_> {
if self.is_main {
UrlResolveKind::Root
} else {
UrlResolveKind::File { file: self }
}
}
}
impl Ord for ShortPath {
fn cmp(&self, other: &Self) -> Ordering {
other
.is_main
.cmp(&self.is_main)
.then_with(|| self.display_name().cmp(other.display_name()))
}
}
impl PartialOrd for ShortPath {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(
Debug,
PartialEq,
Eq,
Hash,
Clone,
Copy,
Serialize,
Deserialize,
Ord,
PartialOrd,
)]
pub enum MethodKind {
Method,
Getter,
Setter,
}
impl From<deno_ast::swc::ast::MethodKind> for MethodKind {
fn from(value: deno_ast::swc::ast::MethodKind) -> Self {
match value {
deno_ast::swc::ast::MethodKind::Method => Self::Method,
deno_ast::swc::ast::MethodKind::Getter => Self::Getter,
deno_ast::swc::ast::MethodKind::Setter => Self::Setter,
}
}
}
#[derive(
Debug,
PartialEq,
Eq,
Hash,
Clone,
Copy,
Serialize,
Deserialize,
Ord,
PartialOrd,
)]
pub enum DrilldownKind {
Property,
Method(MethodKind),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocNodeWithContext {
pub origin: Arc<ShortPath>,
pub ns_qualifiers: Arc<[String]>,
pub inner: Arc<Symbol>,
pub drilldown_name: Option<Box<str>>,
pub drilldown_kind: Option<DrilldownKind>,
#[serde(skip, default)]
pub parent: Option<Arc<DocNodeWithContext>>,
#[serde(skip, default)]
pub namespace_children: Option<Arc<Vec<DocNodeWithContext>>>,
#[serde(skip, default)]
qualified_name: std::sync::OnceLock<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diff_status: Option<DiffStatus>,
}
impl DocNodeWithContext {
pub fn get_kind_ctxs(&self) -> IndexSet<DocNodeKindCtx> {
if let Some(kind) = self.drilldown_kind {
IndexSet::from([kind.into()])
} else {
self
.inner
.declarations
.iter()
.map(|d| DocNodeKindCtx::from(crate::node::DocNodeKind::from(&d.def)))
.collect()
}
}
pub fn create_child(&self, doc_node: Arc<Symbol>) -> Self {
DocNodeWithContext {
origin: self.origin.clone(),
ns_qualifiers: self.ns_qualifiers.clone(),
inner: doc_node,
drilldown_name: None,
drilldown_kind: None,
parent: Some(Arc::new(self.clone())),
namespace_children: None,
qualified_name: std::sync::OnceLock::new(),
diff_status: None,
}
}
fn create_child_with_parent(
parent: Arc<DocNodeWithContext>,
doc_node: Arc<Symbol>,
) -> Self {
DocNodeWithContext {
origin: parent.origin.clone(),
ns_qualifiers: parent.ns_qualifiers.clone(),
inner: doc_node,
drilldown_name: None,
drilldown_kind: None,
parent: Some(parent),
namespace_children: None,
qualified_name: std::sync::OnceLock::new(),
diff_status: None,
}
}
pub fn create_namespace_child(
&self,
doc_node: Arc<Symbol>,
qualifiers: Arc<[String]>,
) -> Self {
let mut child = self.create_child(doc_node);
child.ns_qualifiers = qualifiers;
child
}
pub fn create_child_method(
&self,
mut method_doc_node: Symbol,
is_static: bool,
method_kind: deno_ast::swc::ast::MethodKind,
) -> Self {
let original_name = method_doc_node.name.clone();
method_doc_node.name =
qualify_drilldown_name(self.get_name(), &method_doc_node.name, is_static)
.into_boxed_str();
for decl in &mut method_doc_node.declarations {
decl.declaration_kind = self.inner.declarations[0].declaration_kind;
}
let mut new_node = self.create_child(Arc::new(method_doc_node));
new_node.drilldown_name = Some(original_name);
new_node.drilldown_kind = Some(DrilldownKind::Method(method_kind.into()));
new_node.diff_status = self.diff_status.clone();
new_node
}
fn create_child_method_with_parent(
parent: &Arc<DocNodeWithContext>,
mut method_doc_node: Symbol,
is_static: bool,
method_kind: deno_ast::swc::ast::MethodKind,
) -> Self {
let original_name = method_doc_node.name.clone();
method_doc_node.name = qualify_drilldown_name(
parent.get_name(),
&method_doc_node.name,
is_static,
)
.into_boxed_str();
for decl in &mut method_doc_node.declarations {
decl.declaration_kind = parent.inner.declarations[0].declaration_kind;
}
let mut new_node =
Self::create_child_with_parent(parent.clone(), Arc::new(method_doc_node));
new_node.drilldown_name = Some(original_name);
new_node.drilldown_kind = Some(DrilldownKind::Method(method_kind.into()));
new_node.diff_status = parent.diff_status.clone();
new_node
}
pub fn create_child_property(
&self,
mut property_doc_node: Symbol,
is_static: bool,
) -> Self {
let original_name = property_doc_node.name.clone();
property_doc_node.name = qualify_drilldown_name(
self.get_name(),
&property_doc_node.name,
is_static,
)
.into_boxed_str();
for decl in &mut property_doc_node.declarations {
decl.declaration_kind = self.inner.declarations[0].declaration_kind;
}
let mut new_node = self.create_child(Arc::new(property_doc_node));
new_node.drilldown_name = Some(original_name);
new_node.drilldown_kind = Some(DrilldownKind::Property);
new_node.diff_status = self.diff_status.clone();
new_node
}
fn create_child_property_with_parent(
parent: &Arc<DocNodeWithContext>,
mut property_doc_node: Symbol,
is_static: bool,
) -> Self {
let original_name = property_doc_node.name.clone();
property_doc_node.name = qualify_drilldown_name(
parent.get_name(),
&property_doc_node.name,
is_static,
)
.into_boxed_str();
for decl in &mut property_doc_node.declarations {
decl.declaration_kind = parent.inner.declarations[0].declaration_kind;
}
let mut new_node = Self::create_child_with_parent(
parent.clone(),
Arc::new(property_doc_node),
);
new_node.drilldown_name = Some(original_name);
new_node.drilldown_kind = Some(DrilldownKind::Property);
new_node.diff_status = parent.diff_status.clone();
new_node
}
pub fn get_qualified_name(&self) -> &str {
self.qualified_name.get_or_init(|| {
if self.ns_qualifiers.is_empty() {
self.get_name().to_string()
} else {
format!("{}.{}", self.ns_qualifiers.join("."), self.get_name())
}
})
}
pub fn sub_qualifier(&self) -> Vec<String> {
let mut ns_qualifiers = Vec::from(&*self.ns_qualifiers);
ns_qualifiers.push(self.get_name().to_string());
ns_qualifiers
}
pub fn is_internal(&self, ctx: &GenerateCtx) -> bool {
(self
.inner
.declarations
.iter()
.any(|d| d.declaration_kind == crate::node::DeclarationKind::Private)
&& !matches!(ctx.file_mode, FileMode::SingleDts | FileMode::Dts))
|| self
.inner
.declarations
.iter()
.flat_map(|d| d.js_doc.tags.iter())
.any(|tag| tag == &JsDocTag::Internal)
}
fn get_topmost_ancestor(&self) -> &DocNodeWithContext {
match &self.parent {
Some(parent_node) => parent_node.get_topmost_ancestor(),
None => self,
}
}
fn get_drilldown_symbols(&self) -> Option<Vec<DocNodeWithContext>> {
let declaration_kind = self.inner.declarations[0].declaration_kind;
let mut symbols = Vec::new();
let parent_rc = Arc::new(self.clone());
for decl in &self.inner.declarations {
match &decl.def {
DeclarationDef::Class(class_def) => {
symbols.extend(class_def.methods.iter().map(|method| {
Self::create_child_method_with_parent(
&parent_rc,
Symbol::function(
method.name.clone(),
false,
method.location.clone(),
declaration_kind,
method.js_doc.clone(),
method.function_def.clone(),
),
method.is_static,
method.kind,
)
}));
symbols.extend(class_def.properties.iter().map(|property| {
Self::create_child_property_with_parent(
&parent_rc,
Symbol::from(property.clone()),
property.is_static,
)
}));
}
DeclarationDef::Interface(interface_def) => {
symbols.extend(interface_def.methods.iter().map(|method| {
Self::create_child_method_with_parent(
&parent_rc,
Symbol::from(method.clone()),
true,
method.kind,
)
}));
symbols.extend(interface_def.properties.iter().map(|property| {
Self::create_child_property_with_parent(
&parent_rc,
Symbol::from(property.clone()),
true,
)
}));
}
DeclarationDef::TypeAlias(type_alias_def) => {
if let crate::ts_type::TsTypeDefKind::TypeLiteral(ts_type_literal) =
&type_alias_def.ts_type.kind
{
symbols.extend(ts_type_literal.methods.iter().map(|method| {
Self::create_child_method_with_parent(
&parent_rc,
Symbol::from(method.clone()),
true,
method.kind,
)
}));
symbols.extend(ts_type_literal.properties.iter().map(|property| {
Self::create_child_property_with_parent(
&parent_rc,
Symbol::from(property.clone()),
true,
)
}));
}
}
DeclarationDef::Variable(variable_def) => {
if let Some(crate::ts_type::TsTypeDefKind::TypeLiteral(
ts_type_literal,
)) = variable_def.ts_type.as_ref().map(|ts_type| &ts_type.kind)
{
symbols.extend(ts_type_literal.methods.iter().map(|method| {
Self::create_child_method_with_parent(
&parent_rc,
Symbol::from(method.clone()),
true,
method.kind,
)
}));
symbols.extend(ts_type_literal.properties.iter().map(|property| {
Self::create_child_property_with_parent(
&parent_rc,
Symbol::from(property.clone()),
true,
)
}));
}
}
_ => {}
}
}
if symbols.is_empty() {
None
} else {
Some(symbols)
}
}
}
impl core::ops::Deref for DocNodeWithContext {
type Target = Symbol;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[derive(Default, Debug, Eq, PartialEq)]
pub enum FileMode {
#[default]
Normal,
Dts,
Single,
SingleDts,
}
pub fn generate(
ctx: GenerateCtx,
) -> Result<HashMap<String, String>, anyhow::Error> {
let mut files = HashMap::new();
{
let (partitions_for_entrypoint_nodes, uses_categories) =
if let Some(entrypoint) = ctx.main_entrypoint.as_ref() {
let nodes = ctx.doc_nodes.get(entrypoint).unwrap();
let categories = partition::partition_nodes_by_category(
&ctx,
nodes.iter().map(Cow::Borrowed),
ctx.file_mode == FileMode::SingleDts,
);
if categories.len() == 1 && categories.contains_key("Uncategorized") {
(
partition::partition_nodes_by_kind(
&ctx,
nodes.iter().map(Cow::Borrowed),
ctx.file_mode == FileMode::SingleDts,
),
false,
)
} else {
(categories, true)
}
} else {
Default::default()
};
let index = pages::IndexCtx::new(
&ctx,
ctx.main_entrypoint.clone(),
partitions_for_entrypoint_nodes,
uses_categories,
);
files.insert(
"./index.html".to_string(),
ctx.render(pages::IndexCtx::TEMPLATE, &index),
);
}
{
let all_symbols = pages::AllSymbolsPageCtx::new(&ctx);
files.insert(
"./all_symbols.html".to_string(),
ctx.render(pages::AllSymbolsPageCtx::TEMPLATE, &all_symbols),
);
}
if ctx.file_mode == FileMode::SingleDts {
let all_doc_nodes = ctx
.doc_nodes
.values()
.flatten()
.cloned()
.collect::<Vec<DocNodeWithContext>>();
let categories = partition::partition_nodes_by_category(
&ctx,
all_doc_nodes.iter().map(Cow::Borrowed),
true,
);
if categories.len() != 1 {
for (category, nodes) in &categories {
let partitions = partition::partition_nodes_by_kind(
&ctx,
nodes.iter().map(Cow::Borrowed),
false,
);
let index = pages::IndexCtx::new_category(
&ctx,
category,
partitions,
&all_doc_nodes,
);
files.insert(
format!("{}.html", util::slugify(category)),
ctx.render(pages::IndexCtx::TEMPLATE, &index),
);
}
}
}
{
for (short_path, doc_nodes) in &ctx.doc_nodes {
let doc_nodes_by_kind = partition::partition_nodes_by_kind(
&ctx,
doc_nodes.iter().map(Cow::Borrowed),
ctx.file_mode == FileMode::SingleDts,
);
let symbol_pages =
generate_symbol_pages_for_module(&ctx, short_path, doc_nodes);
files.extend(symbol_pages.into_iter().flat_map(|symbol_page| {
match symbol_page {
SymbolPage::Symbol {
breadcrumbs_ctx,
symbol_group_ctx,
toc_ctx,
categories_panel,
} => {
let root = ctx.resolve_path(
UrlResolveKind::Symbol {
file: short_path,
symbol: &symbol_group_ctx.name,
},
UrlResolveKind::Root,
);
let mut title_parts = breadcrumbs_ctx.to_strings();
title_parts.reverse();
title_parts.pop();
let html_head_ctx = pages::HtmlHeadCtx::new(
&ctx,
&root,
Some(&title_parts.join(" - ")),
Some(short_path),
);
let file_name =
format!("{}/~/{}.html", short_path.path, symbol_group_ctx.name);
let page_ctx = pages::SymbolPageCtx {
html_head_ctx,
symbol_group_ctx,
breadcrumbs_ctx,
toc_ctx,
disable_search: ctx.disable_search,
categories_panel,
};
let symbol_page =
ctx.render(pages::SymbolPageCtx::TEMPLATE, &page_ctx);
vec![(file_name, symbol_page)]
}
SymbolPage::Redirect {
current_symbol,
href,
..
} => {
let redirect =
serde_json::json!({ "kind": "redirect", "path": href });
let file_name =
format!("{}/~/{}.html", short_path.path, current_symbol);
vec![(file_name, ctx.render("pages/redirect", &redirect))]
}
}
}));
if !short_path.is_main {
let index = pages::IndexCtx::new(
&ctx,
Some(short_path.clone()),
doc_nodes_by_kind,
false,
);
files.insert(
format!("{}/index.html", short_path.path),
ctx.render(pages::IndexCtx::TEMPLATE, &index),
);
}
}
}
files.insert(STYLESHEET_FILENAME.into(), STYLESHEET.into());
files.insert(
SEARCH_INDEX_FILENAME.into(),
search::get_search_index_file(&ctx)?,
);
files.insert(SCRIPT_FILENAME.into(), SCRIPT_JS.into());
files.insert(PAGE_STYLESHEET_FILENAME.into(), PAGE_STYLESHEET.into());
files.insert(RESET_STYLESHEET_FILENAME.into(), RESET_STYLESHEET.into());
files.insert(FUSE_FILENAME.into(), FUSE_JS.into());
files.insert(SEARCH_FILENAME.into(), SEARCH_JS.into());
files.insert(DARKMODE_TOGGLE_FILENAME.into(), DARKMODE_TOGGLE_JS.into());
#[cfg(feature = "comrak")]
files.insert(
comrak::COMRAK_STYLESHEET_FILENAME.into(),
comrak::COMRAK_STYLESHEET.into(),
);
Ok(files)
}
pub fn generate_json_with<F>(
ctx: GenerateCtx,
mut emit: F,
) -> Result<(), anyhow::Error>
where
F: FnMut(String, String),
{
let diff_only = ctx.diff_only;
let approx_pages: usize = ctx.doc_nodes.values().map(|v| v.len()).sum();
let mut emitted_keys =
std::collections::HashSet::with_capacity(approx_pages * 2);
{
let (partitions_for_entrypoint_nodes, uses_categories) =
if let Some(entrypoint) = ctx.main_entrypoint.as_ref() {
let nodes = ctx.doc_nodes.get(entrypoint).unwrap();
let categories = partition::partition_nodes_by_category(
&ctx,
nodes.iter().map(Cow::Borrowed),
ctx.file_mode == FileMode::SingleDts,
);
if categories.len() == 1 && categories.contains_key("Uncategorized") {
(
partition::partition_nodes_by_kind(
&ctx,
nodes.iter().map(Cow::Borrowed),
ctx.file_mode == FileMode::SingleDts,
),
false,
)
} else {
(categories, true)
}
} else {
Default::default()
};
let index = pages::IndexCtx::new(
&ctx,
ctx.main_entrypoint.clone(),
partitions_for_entrypoint_nodes,
uses_categories,
);
if !diff_only
|| index
.overview
.as_ref()
.is_some_and(|o| !o.sections.is_empty())
|| index
.module_doc
.as_ref()
.is_some_and(|md| !md.sections.sections.is_empty())
{
emit("./index.json".to_string(), serde_json::to_string(&index)?);
}
}
{
let all_symbols = pages::AllSymbolsPageCtx::new(&ctx);
if !diff_only || !all_symbols.content.entrypoints.is_empty() {
emit(
"./all_symbols.json".to_string(),
serde_json::to_string(&all_symbols)?,
);
}
}
if ctx.file_mode == FileMode::SingleDts {
let all_doc_nodes = ctx
.doc_nodes
.values()
.flatten()
.cloned()
.collect::<Vec<DocNodeWithContext>>();
let categories = partition::partition_nodes_by_category(
&ctx,
all_doc_nodes.iter().map(Cow::Borrowed),
true,
);
if categories.len() != 1 {
for (category, nodes) in &categories {
let partitions = partition::partition_nodes_by_kind(
&ctx,
nodes.iter().map(Cow::Borrowed),
false,
);
let index = pages::IndexCtx::new_category(
&ctx,
category,
partitions,
&all_doc_nodes,
);
if diff_only
&& index
.overview
.as_ref()
.is_none_or(|o| o.sections.is_empty())
{
continue;
}
emit(
format!("{}.json", util::slugify(category)),
serde_json::to_string(&index)?,
);
}
}
}
{
for (short_path, doc_nodes) in &ctx.doc_nodes {
let doc_nodes_by_kind = partition::partition_nodes_by_kind(
&ctx,
doc_nodes.iter().map(Cow::Borrowed),
ctx.file_mode == FileMode::SingleDts,
);
let symbol_pages =
generate_symbol_pages_for_module(&ctx, short_path, doc_nodes);
for symbol_page in symbol_pages {
match symbol_page {
SymbolPage::Symbol {
breadcrumbs_ctx,
symbol_group_ctx,
toc_ctx,
categories_panel,
} => {
if diff_only && symbol_group_ctx.diff_status.is_none() {
continue;
}
let file_name =
format!("{}/~/{}.json", short_path.path, symbol_group_ctx.name);
if !emitted_keys.insert(file_name.clone()) {
continue;
}
let mut symbol_group_ctx = symbol_group_ctx;
if diff_only {
symbol_group_ctx.strip_unchanged_tags();
}
let root = ctx.resolve_path(
UrlResolveKind::Symbol {
file: short_path,
symbol: &symbol_group_ctx.name,
},
UrlResolveKind::Root,
);
let mut title_parts = breadcrumbs_ctx.to_strings();
title_parts.reverse();
title_parts.pop();
let html_head_ctx = pages::HtmlHeadCtx::new(
&ctx,
&root,
Some(&title_parts.join(" - ")),
Some(short_path),
);
let page_ctx = pages::SymbolPageCtx {
html_head_ctx,
symbol_group_ctx,
breadcrumbs_ctx,
toc_ctx,
disable_search: ctx.disable_search,
categories_panel,
};
emit(file_name, serde_json::to_string(&page_ctx)?);
}
SymbolPage::Redirect {
current_symbol,
href,
diff_status,
} => {
if diff_only && diff_status.is_none() {
continue;
}
let file_name =
format!("{}/~/{}.json", short_path.path, current_symbol);
if !emitted_keys.insert(file_name.clone()) {
continue;
}
let redirect = serde_json::json!({ "path": href });
emit(file_name, serde_json::to_string(&redirect)?);
}
}
}
if !short_path.is_main {
let index = pages::IndexCtx::new(
&ctx,
Some(short_path.clone()),
doc_nodes_by_kind,
false,
);
if diff_only
&& index
.overview
.as_ref()
.is_none_or(|o| o.sections.is_empty())
&& index
.module_doc
.as_ref()
.is_none_or(|md| md.sections.sections.is_empty())
{
continue;
}
emit(
format!("{}/index.json", short_path.path),
serde_json::to_string(&index)?,
);
}
}
}
if !diff_only {
let search_index = generate_search_index(&ctx);
emit("search.json".into(), serde_json::to_string(&search_index)?);
}
Ok(())
}
pub fn generate_json(
ctx: GenerateCtx,
) -> Result<HashMap<String, String>, anyhow::Error> {
let mut files = HashMap::new();
generate_json_with(ctx, |name, content| {
files.insert(name, content);
})?;
Ok(files)
}
pub fn find_common_ancestor<'a>(
urls: impl Iterator<Item = &'a ModuleSpecifier>,
single_file_is_common_ancestor: bool,
) -> Option<PathBuf> {
let paths: Vec<PathBuf> = urls
.filter_map(|url| {
if url.scheme() == "file" {
deno_path_util::url_to_file_path(url).ok()
} else {
None
}
})
.collect();
if paths.is_empty() || paths.len() == 1 && !single_file_is_common_ancestor {
return None;
}
let shortest_path = paths
.iter()
.min_by_key(|path| path.components().count())
.unwrap();
let mut common_ancestor = PathBuf::new();
for (index, component) in shortest_path.components().enumerate() {
if paths.iter().all(|path| {
path.components().count() > index
&& path.components().nth(index) == Some(component)
}) {
common_ancestor.push(component);
} else {
break;
}
}
if common_ancestor.as_os_str().is_empty()
|| common_ancestor == PathBuf::from("/")
{
None
} else {
Some(common_ancestor)
}
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(not(windows))]
#[test]
fn common_ancestor_root() {
run_common_ancestor_test(
&[
"file:///bytes.ts",
"file:///colors.ts",
"file:///duration.ts",
"file:///printf.ts",
],
false,
None,
);
}
#[test]
fn common_ancestor_single_file() {
run_common_ancestor_test(&["file:///a/a.ts"], false, None);
}
#[test]
fn common_ancestor_multiple_files() {
run_common_ancestor_test(
&["file:///a/a.ts", "file:///a/b.ts"],
false,
Some("file:///a/"),
);
}
#[test]
fn common_ancestor_single_file_single_mode() {
run_common_ancestor_test(&["file:///a/a.ts"], true, Some("file:///a/a.ts"));
}
#[test]
fn common_ancestor_multiple_file_single_mode() {
run_common_ancestor_test(
&["file:///a/a.ts", "file:///a/b.ts"],
true,
Some("file:///a/"),
);
}
#[track_caller]
fn run_common_ancestor_test(
specifiers: &[&str],
single_file_is_common_ancestor: bool,
expected: Option<&str>,
) {
let map = specifiers
.iter()
.map(|specifier| normalize_specifier(specifier))
.collect::<Vec<_>>();
let common_ancestor =
find_common_ancestor(map.iter(), single_file_is_common_ancestor);
assert_eq!(
common_ancestor,
expected.map(|e| normalize_specifier(e).to_file_path().unwrap()),
);
}
fn normalize_specifier(specifier: &str) -> ModuleSpecifier {
if cfg!(windows) {
ModuleSpecifier::parse(&specifier.replace("file:///", "file:///c:/"))
.unwrap()
} else {
ModuleSpecifier::parse(specifier).unwrap()
}
}
}