diff --git a/js/mod.ts b/js/mod.ts index e8965937..dae2bad4 100644 --- a/js/mod.ts +++ b/js/mod.ts @@ -165,9 +165,13 @@ export type UrlResolveKind = | UrlResolveKindFile | UrlResolveKindSymbol; -interface HrefResolver { - /** Resolver for how files should link to eachother. */ - resolvePath?(current: UrlResolveKind, target: UrlResolveKind): string; +export interface HrefResolver { + /** Resolver for how files should link to each other. */ + resolvePath?( + current: UrlResolveKind, + target: UrlResolveKind, + defaultResolve: () => string, + ): string; /** Resolver for global symbols, like the Deno namespace or other built-ins */ resolveGlobalSymbol?(symbol: string[]): string | undefined; /** Resolver for symbols from non-relative imports */ @@ -212,7 +216,7 @@ export interface UsageComposer { ): Map; } -interface GenerateOptions { +export interface GenerateOptions { /** The name of the package to use in the breadcrumbs. */ packageName?: string; /** The main entrypoint if one is present. */ @@ -236,10 +240,9 @@ interface GenerateOptions { */ symbolRedirectMap?: Record>; /** - * Map of modules, where the value is a link to where the default symbol - * should redirect to. + * Map of modules, where the value is what the name of the default symbol should be. */ - defaultRedirectMap?: Record; + defaultSymbolMap?: Record; /** * Hook to inject content in the `head` tag. * @@ -300,8 +303,8 @@ const defaultUsageComposer: UsageComposer = { * @param docNodesByUrl DocNodes keyed by their absolute URL. */ export async function generateHtml( - options: GenerateOptions, docNodesByUrl: Record>, + options: GenerateOptions, ): Promise> { const { usageComposer = defaultUsageComposer, @@ -317,7 +320,7 @@ export async function generateHtml( options.categoryDocs, options.disableSearch ?? false, options.symbolRedirectMap, - options.defaultRedirectMap, + options.defaultSymbolMap, options.hrefResolver?.resolvePath, options.hrefResolver?.resolveGlobalSymbol || (() => undefined), options.hrefResolver?.resolveImportHref || (() => undefined), diff --git a/js/test.ts b/js/test.ts index 5c997370..f719a13d 100644 --- a/js/test.ts +++ b/js/test.ts @@ -131,7 +131,7 @@ Deno.test({ "https://deno.land/std@0.104.0/fmt/colors.ts", ); - const files = await generateHtml({ + const files = await generateHtml({ ["file:///colors.ts"]: entries }, { markdownRenderer( md, _titleOnly, @@ -143,7 +143,7 @@ Deno.test({ markdownStripper(md: string) { return md; }, - }, { ["file:///colors.ts"]: entries }); + }); assertEquals(Object.keys(files).length, 61); }, diff --git a/lib/lib.rs b/lib/lib.rs index 67933234..929f5446 100644 --- a/lib/lib.rs +++ b/lib/lib.rs @@ -23,6 +23,7 @@ use import_map::ImportMap; use import_map::ImportMapOptions; use indexmap::IndexMap; use serde::Serialize; +use std::ffi::c_void; use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; @@ -37,6 +38,11 @@ macro_rules! console_warn { ($($t:tt)*) => (warn(&format_args!($($t)*).to_string())) } +thread_local! { + static CURRENT: std::cell::RefCell<*const c_void> = const { std::cell::RefCell::new(std::ptr::null()) }; + static TARGET: std::cell::RefCell<*const c_void> = const { std::cell::RefCell::new(std::ptr::null()) }; +} + struct JsLoader { load: js_sys::Function, } @@ -318,13 +324,71 @@ impl deno_doc::html::HrefResolver for JsHrefResolver { if let Some(resolve_path) = &self.resolve_path { let this = JsValue::null(); + let new_current = current.clone(); + let new_target = target.clone(); + + { + let current_ptr = + &new_current as *const UrlResolveKind as *const c_void; + CURRENT.set(current_ptr); + let target = &new_target as *const UrlResolveKind as *const c_void; + TARGET.set(target); + } + + let default_closure = Box::new(move || { + CURRENT.with(|current| { + let current_ptr = *current.borrow() as *const UrlResolveKind; + assert!(!current_ptr.is_null()); + // SAFETY: this pointer is valid until destroyed, which is done + // after compose is called + let current_val = unsafe { &*current_ptr }; + + let path = TARGET.with(|target| { + let target_ptr = *target.borrow() as *const UrlResolveKind; + assert!(!target_ptr.is_null()); + // SAFETY: this pointer is valid until destroyed, which is done + // after compose is called + let target_val = unsafe { &*target_ptr }; + + let path = + deno_doc::html::href_path_resolve(*current_val, *target_val); + + *target.borrow_mut() = + target_val as *const UrlResolveKind as *const c_void; + + path + }); + + *current.borrow_mut() = + current_val as *const UrlResolveKind as *const c_void; + + path + }) + }); + + let default_closure = + Closure::wrap(Box::new(default_closure) as Box String>); + let default_closure = + JsCast::unchecked_ref::(default_closure.as_ref()); + let current = serde_wasm_bindgen::to_value(¤t).unwrap(); let target = serde_wasm_bindgen::to_value(&target).unwrap(); let global_symbol = resolve_path - .call2(&this, ¤t, &target) + .call3(&this, ¤t, &target, default_closure) .expect("resolve_path errored"); + { + let current = + CURRENT.replace(std::ptr::null()) as *const UrlResolveKind; + // SAFETY: take the pointer and drop it + let _ = unsafe { &*current }; + + let target = TARGET.replace(std::ptr::null()) as *const UrlResolveKind; + // SAFETY: take the pointer and drop it + let _ = unsafe { &*target }; + } + serde_wasm_bindgen::from_value(global_symbol) .expect("resolve_path returned an invalid value") } else { diff --git a/src/html/mod.rs b/src/html/mod.rs index 3fe9b2b9..e7d56035 100644 --- a/src/html/mod.rs +++ b/src/html/mod.rs @@ -493,7 +493,7 @@ impl ShortPath { if self.is_main { UrlResolveKind::Root } else { - UrlResolveKind::File(self) + UrlResolveKind::File { file: self } } } } diff --git a/src/html/pages.rs b/src/html/pages.rs index 21b40d90..7dae6145 100644 --- a/src/html/pages.rs +++ b/src/html/pages.rs @@ -113,7 +113,7 @@ impl CategoriesPanelCtx { name: short_path.display_name().to_string(), href: ctx.ctx.resolve_path( ctx.get_current_resolve(), - UrlResolveKind::File(short_path), + UrlResolveKind::File { file: short_path }, ), active: current_path.is_some_and(|current_path| { current_path == short_path.display_name() @@ -157,7 +157,7 @@ impl CategoriesPanelCtx { .map(|title| CategoriesPanelCategoryCtx { href: ctx.ctx.resolve_path( ctx.get_current_resolve(), - UrlResolveKind::Category(&title), + UrlResolveKind::Category { category: &title }, ), active: current_path .is_some_and(|current_path| current_path == title), @@ -320,7 +320,7 @@ impl IndexCtx { header: SectionHeaderCtx { href: Some(render_ctx.ctx.resolve_path( render_ctx.get_current_resolve(), - UrlResolveKind::Category(&title), + UrlResolveKind::Category { category: &title }, )), title, anchor: AnchorCtx { id: anchor }, @@ -376,8 +376,11 @@ impl IndexCtx { partitions: partition::Partitions, all_doc_nodes: &[DocNodeWithContext], ) -> Self { - let render_ctx = - RenderContext::new(ctx, all_doc_nodes, UrlResolveKind::Category(name)); + let render_ctx = RenderContext::new( + ctx, + all_doc_nodes, + UrlResolveKind::Category { category: name }, + ); let sections = super::namespace::render_namespace( partitions.into_iter().map(|(title, nodes)| { @@ -398,8 +401,10 @@ impl IndexCtx { }), ); - let root = - ctx.resolve_path(UrlResolveKind::Category(name), UrlResolveKind::Root); + let root = ctx.resolve_path( + UrlResolveKind::Category { category: name }, + UrlResolveKind::Root, + ); let html_head_ctx = HtmlHeadCtx::new(ctx, &root, Some(name), None); @@ -515,8 +520,11 @@ pub fn generate_symbol_pages_for_module( let mut generated_pages = Vec::with_capacity(name_partitions.values().len()); - let render_ctx = - RenderContext::new(ctx, module_doc_nodes, UrlResolveKind::File(short_path)); + let render_ctx = RenderContext::new( + ctx, + module_doc_nodes, + UrlResolveKind::File { file: short_path }, + ); for (name, doc_nodes) in name_partitions { let (breadcrumbs_ctx, symbol_group_ctx, toc_ctx, categories_panel) = diff --git a/src/html/render_context.rs b/src/html/render_context.rs index 0c3bc8d7..bfe735ed 100644 --- a/src/html/render_context.rs +++ b/src/html/render_context.rs @@ -199,7 +199,7 @@ impl<'ctx> RenderContext<'ctx> { }, ] } - UrlResolveKind::Category(category) => { + UrlResolveKind::Category { category } => { vec![ BreadcrumbCtx { name: index_name, @@ -217,7 +217,7 @@ impl<'ctx> RenderContext<'ctx> { }, ] } - UrlResolveKind::File(file) => { + UrlResolveKind::File { file } => { if file.is_main { vec![BreadcrumbCtx { name: index_name, @@ -257,9 +257,10 @@ impl<'ctx> RenderContext<'ctx> { if !file.is_main { parts.push(BreadcrumbCtx { name: file.display_name().to_string(), - href: self - .ctx - .resolve_path(self.current_resolve, UrlResolveKind::File(file)), + href: self.ctx.resolve_path( + self.current_resolve, + UrlResolveKind::File { file }, + ), is_symbol: false, is_first_symbol: false, }); @@ -268,7 +269,7 @@ impl<'ctx> RenderContext<'ctx> { name: category.to_string(), href: self.ctx.resolve_path( self.current_resolve, - UrlResolveKind::Category(category), + UrlResolveKind::Category { category }, ), is_symbol: false, is_first_symbol: false, @@ -618,8 +619,11 @@ mod test { let render_ctx = RenderContext::new(&ctx, doc_nodes, UrlResolveKind::Root); assert_eq!(render_ctx.lookup_symbol_href("foo").unwrap(), "b/foo"); - let render_ctx = - RenderContext::new(&ctx, doc_nodes, UrlResolveKind::File(short_path)); + let render_ctx = RenderContext::new( + &ctx, + doc_nodes, + UrlResolveKind::File { file: short_path }, + ); assert_eq!(render_ctx.lookup_symbol_href("foo").unwrap(), "b/foo"); } } diff --git a/src/html/usage.rs b/src/html/usage.rs index df434c91..d879c8c1 100644 --- a/src/html/usage.rs +++ b/src/html/usage.rs @@ -233,12 +233,14 @@ impl UsagesCtx { move |url: String, custom_file_identifier: Option| { RENDER_CONTEXT.with(|ctx| { let render_ctx_ptr = *ctx.borrow() as *const RenderContext; + assert!(!render_ctx_ptr.is_null()); // SAFETY: this pointer is valid until destroyed, which is done // after compose is called let render_ctx = unsafe { &*render_ctx_ptr }; let usage = DOC_NODES.with(|nodes| { let (nodes_ptr, nodes_ptr_len) = *nodes.borrow(); + assert!(!nodes_ptr.is_null()); // SAFETY: the pointers are valid until destroyed, which is done // after compose is called let doc_nodes = unsafe { diff --git a/src/html/util.rs b/src/html/util.rs index 89a51a8f..381640f3 100644 --- a/src/html/util.rs +++ b/src/html/util.rs @@ -232,8 +232,12 @@ impl NamespacedGlobalSymbols { pub enum UrlResolveKind<'a> { Root, AllSymbols, - Category(&'a str), - File(&'a ShortPath), + Category { + category: &'a str, + }, + File { + file: &'a ShortPath, + }, Symbol { file: &'a ShortPath, symbol: &'a str, @@ -245,8 +249,8 @@ impl UrlResolveKind<'_> { match self { UrlResolveKind::Root => None, UrlResolveKind::AllSymbols => None, - UrlResolveKind::Category(_) => None, - UrlResolveKind::File(file) => Some(file), + UrlResolveKind::Category { .. } => None, + UrlResolveKind::File { file } => Some(file), UrlResolveKind::Symbol { file, .. } => Some(file), } } @@ -257,7 +261,7 @@ pub fn href_path_resolve( target: UrlResolveKind, ) -> String { let backs = match current { - UrlResolveKind::File(file) => "../".repeat(if file.is_main { + UrlResolveKind::File { file } => "../".repeat(if file.is_main { 1 } else { file.path.split('/').count() @@ -269,12 +273,12 @@ pub fn href_path_resolve( }), UrlResolveKind::Root => String::new(), UrlResolveKind::AllSymbols => String::from("./"), - UrlResolveKind::Category(_) => String::from("./"), + UrlResolveKind::Category { .. } => String::from("./"), }; match target { UrlResolveKind::Root => backs, - UrlResolveKind::File(target_file) if target_file.is_main => backs, + UrlResolveKind::File { file: target_file } if target_file.is_main => backs, UrlResolveKind::AllSymbols => format!("{backs}./all_symbols.html"), UrlResolveKind::Symbol { file: target_file, @@ -283,10 +287,10 @@ pub fn href_path_resolve( } => { format!("{backs}./{}/~/{target_symbol}.html", target_file.path) } - UrlResolveKind::File(target_file) => { + UrlResolveKind::File { file: target_file } => { format!("{backs}./{}/index.html", target_file.path) } - UrlResolveKind::Category(category) => { + UrlResolveKind::Category { category } => { format!("{backs}./{}.html", slugify(category)) } } diff --git a/tests/html_test.rs b/tests/html_test.rs index ac9d7edc..4e0a27cc 100644 --- a/tests/html_test.rs +++ b/tests/html_test.rs @@ -511,8 +511,11 @@ async fn module_doc() { let mut module_docs = vec![]; for (short_path, doc_nodes) in &ctx.doc_nodes { - let render_ctx = - RenderContext::new(&ctx, doc_nodes, UrlResolveKind::File(short_path)); + let render_ctx = RenderContext::new( + &ctx, + doc_nodes, + UrlResolveKind::File { file: short_path }, + ); let module_doc = jsdoc::ModuleDocCtx::new(&render_ctx, short_path); module_docs.push(module_doc);