Skip to content

Commit

Permalink
feat(html): allow generating JSON representation of HTML output (#686)
Browse files Browse the repository at this point in the history
  • Loading branch information
crowlKats authored Jan 4, 2025
1 parent 6553991 commit f559dbe
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 8 deletions.
41 changes: 40 additions & 1 deletion js/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,8 @@ const defaultUsageComposer: UsageComposer = {

/**
* Generate HTML files for provided {@linkcode DocNode}s.
* @param options Options for the generation.
* @param docNodesByUrl DocNodes keyed by their absolute URL.
* @param options Options for the generation.
*/
export async function generateHtml(
docNodesByUrl: Record<string, Array<DocNode>>,
Expand Down Expand Up @@ -330,5 +330,44 @@ export async function generateHtml(
options.markdownStripper,
options.headInject,
docNodesByUrl,
false,
);
}

/**
* Generate JSON data equivalent to the HTML files generated by {@linkcode generateHtml}.
* @param docNodesByUrl DocNodes keyed by their absolute URL.
* @param options Options for the generation.
*/
export async function generateHtmlAsJSON(
docNodesByUrl: Record<string, Array<DocNode>>,
options: GenerateOptions,
// deno-lint-ignore no-explicit-any
): Promise<Record<string, any>> {
const {
usageComposer = defaultUsageComposer,
} = options;

const wasm = await instantiate();
return wasm.generate_html(
options.packageName,
options.mainEntrypoint,
usageComposer.singleMode,
usageComposer.compose,
options.rewriteMap,
options.categoryDocs,
options.disableSearch ?? false,
options.symbolRedirectMap,
options.defaultSymbolMap,
options.hrefResolver?.resolvePath,
options.hrefResolver?.resolveGlobalSymbol || (() => undefined),
options.hrefResolver?.resolveImportHref || (() => undefined),
options.hrefResolver?.resolveSource || (() => undefined),
options.hrefResolver?.resolveExternalJsdocModule || (() => undefined),
options.markdownRenderer,
options.markdownStripper,
options.headInject,
docNodesByUrl,
true,
);
}
29 changes: 28 additions & 1 deletion js/test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { assert, assertEquals, assertRejects } from "jsr:@std/[email protected]";
import { doc, generateHtml } from "./mod.ts";
import { doc, generateHtml, generateHtmlAsJSON } from "./mod.ts";

Deno.test({
name: "doc()",
Expand Down Expand Up @@ -152,3 +152,30 @@ Deno.test({
assertEquals(Object.keys(files).length, 61);
},
});

Deno.test({
name: "generateHtmlAsJSON()",
async fn() {
const entries = await doc(
["https://deno.land/[email protected]/fmt/colors.ts"],
);

const files = await generateHtmlAsJSON({
["file:///colors.ts"]: Object.values(entries)[0],
}, {
markdownRenderer(
md,
_titleOnly,
_filePath,
_anchorizer,
) {
return md;
},
markdownStripper(md: string) {
return md;
},
});

assertEquals(Object.keys(files).length, 55);
},
});
29 changes: 23 additions & 6 deletions lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ pub fn generate_html(
head_inject: Option<js_sys::Function>,

doc_nodes_by_url: JsValue,

json: bool,
) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();

Expand All @@ -307,6 +309,7 @@ pub fn generate_html(
markdown_stripper,
head_inject,
doc_nodes_by_url,
json,
)
.map_err(|err| JsValue::from(js_sys::Error::new(&err.to_string())))
}
Expand Down Expand Up @@ -522,6 +525,8 @@ fn generate_html_inner(
head_inject: Option<js_sys::Function>,

doc_nodes_by_url: JsValue,

json: bool,
) -> Result<JsValue, anyhow::Error> {
let main_entrypoint = main_entrypoint
.map(|s| ModuleSpecifier::parse(&s))
Expand Down Expand Up @@ -631,12 +636,24 @@ fn generate_html_inner(
},
doc_nodes_by_url,
)?;
let files = deno_doc::html::generate(ctx)?;

let serializer =
serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
if json {
let files = deno_doc::html::generate_json(ctx)?;

let serializer =
serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);

files
.serialize(&serializer)
.map_err(|err| anyhow!("{}", err))
files
.serialize(&serializer)
.map_err(|err| anyhow!("{}", err))
} else {
let files = deno_doc::html::generate(ctx)?;

let serializer =
serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);

files
.serialize(&serializer)
.map_err(|err| anyhow!("{}", err))
}
}
184 changes: 184 additions & 0 deletions src/html/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,190 @@ pub fn generate(
Ok(files)
}

pub fn generate_json(
ctx: GenerateCtx,
) -> Result<HashMap<String, serde_json::Value>, anyhow::Error> {
let mut files = HashMap::new();

// Index page
{
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.json".to_string(), serde_json::to_value(index)?);
}

let all_doc_nodes = ctx
.doc_nodes
.values()
.flatten()
.cloned()
.collect::<Vec<DocNodeWithContext>>();

// All symbols (list of all symbols in all files)
{
let partitions_by_kind = partition::partition_nodes_by_entrypoint(
&ctx,
all_doc_nodes.iter().map(Cow::Borrowed),
true,
);

let all_symbols = pages::AllSymbolsCtx::new(&ctx, partitions_by_kind);

files.insert(
"./all_symbols.json".to_string(),
serde_json::to_value(all_symbols)?,
);
}

// Category pages
if ctx.file_mode == FileMode::SingleDts {
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!("{}.json", util::slugify(category)),
serde_json::to_value(index)?,
);
}
}
}

// Pages for all discovered symbols
{
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();
// contains the package name, which we already render in the head
title_parts.pop();

let html_head_ctx = pages::HtmlHeadCtx::new(
&ctx,
&root,
Some(&title_parts.join(" - ")),
Some(short_path),
);

let file_name =
format!("{}/~/{}.json", 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,
};

vec![(file_name, serde_json::to_value(page_ctx).unwrap())]
}
SymbolPage::Redirect {
current_symbol,
href,
} => {
let redirect = serde_json::json!({ "path": href });

let file_name =
format!("{}/~/{}.json", short_path.path, current_symbol);

vec![(file_name, 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.json", short_path.path),
serde_json::to_value(index)?,
);
}
}
}

files.insert("search.json".into(), generate_search_index(&ctx));

Ok(files)
}

pub fn find_common_ancestor<'a>(
urls: impl Iterator<Item = &'a ModuleSpecifier>,
single_file_is_common_ancestor: bool,
Expand Down

0 comments on commit f559dbe

Please sign in to comment.