Skip to content

Commit 53c86dd

Browse files
authored
feat: support html generation via Wasm (#658)
1 parent f8b8d8b commit 53c86dd

File tree

14 files changed

+725
-48
lines changed

14 files changed

+725
-48
lines changed

Cargo.lock

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ name = "deno_doc"
2222

2323
[[example]]
2424
name = "ddoc"
25-
required-features = ["html", "comrak"]
25+
required-features = ["comrak"]
2626

2727
[dependencies]
2828
anyhow = "1.0.86"
@@ -38,9 +38,10 @@ serde.workspace = true
3838
serde_json = { version = "1.0.122", features = ["preserve_order"] }
3939
termcolor = "1.4.1"
4040
itoa = "1.0.11"
41+
deno_path_util = "0.2.1"
4142

42-
html-escape = { version = "0.2.13", optional = true }
43-
handlebars = { version = "6.1", features = ["string_helpers"], optional = true }
43+
html-escape = { version = "0.2.13" }
44+
handlebars = { version = "6.1", features = ["string_helpers"] }
4445
comrak = { version = "0.29.0", optional = true, default-features = false }
4546
ammonia = { version = "4.0.0", optional = true }
4647

@@ -54,10 +55,16 @@ tokio = { version = "1.39.2", features = ["full"] }
5455
pretty_assertions = "1.4.0"
5556
insta = { version = "1.39.0", features = ["json"] }
5657

58+
[target.'cfg(target_arch = "wasm32")'.dependencies]
59+
url = "2.5.2"
60+
percent-encoding = "2.3.1"
61+
wasm-bindgen = "0.2.92"
62+
js-sys = "0.3.69"
63+
serde-wasm-bindgen = "=0.5.0"
64+
5765
[features]
58-
default = ["rust", "html", "comrak"]
66+
default = ["rust", "comrak"]
5967
rust = []
60-
html = ["html-escape", "handlebars"]
6168
comrak = ["dep:comrak", "ammonia"]
6269

6370
[[test]]

examples/ddoc/main.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
use clap::App;
44
use clap::Arg;
55
use deno_doc::find_nodes_by_name_recursively;
6+
use deno_doc::html::HrefResolver;
67
use deno_doc::html::UrlResolveKind;
7-
use deno_doc::html::{
8-
DocNodeWithContext, HrefResolver, UsageComposer, UsageComposerEntry,
9-
};
8+
use deno_doc::html::UsageComposer;
9+
use deno_doc::html::UsageComposerEntry;
1010
use deno_doc::DocNodeKind;
1111
use deno_doc::DocParser;
1212
use deno_doc::DocParserOptions;
@@ -216,7 +216,6 @@ impl UsageComposer for EmptyResolver {
216216

217217
fn compose(
218218
&self,
219-
nodes: &[DocNodeWithContext],
220219
current_resolve: UrlResolveKind,
221220
usage_to_md: deno_doc::html::UsageToMd,
222221
) -> IndexMap<UsageComposerEntry, String> {
@@ -228,7 +227,7 @@ impl UsageComposer for EmptyResolver {
228227
name: "".to_string(),
229228
icon: None,
230229
},
231-
usage_to_md(nodes, current_file.specifier.as_str(), None),
230+
usage_to_md(current_file.specifier.as_str(), None),
232231
)])
233232
})
234233
.unwrap_or_default()

js/mod.ts

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
22

33
import { instantiate } from "./deno_doc_wasm.generated.js";
4-
import type { DocNode } from "./types.d.ts";
4+
import type { DocNode, Location } from "./types.d.ts";
55
import { createCache } from "jsr:@deno/cache-dir@0.11";
66
import type { CacheSetting, LoadResponse } from "jsr:@deno/graph@0.82";
77

@@ -124,3 +124,208 @@ export async function doc(
124124
printImportMapDiagnostics,
125125
);
126126
}
127+
128+
export interface ShortPath {
129+
/** Name identifier for the path. */
130+
path: string;
131+
/** URL for the path. */
132+
specifier: string;
133+
/** Whether the path is the main entrypoint. */
134+
isMain: boolean;
135+
}
136+
137+
export interface UrlResolveKindRoot {
138+
kind: "root";
139+
}
140+
141+
export interface UrlResolveKindAllSymbols {
142+
kind: "allSymbols";
143+
}
144+
145+
export interface UrlResolveKindCategory {
146+
kind: "category";
147+
category: string;
148+
}
149+
150+
export interface UrlResolveKindFile {
151+
kind: "file";
152+
file: ShortPath;
153+
}
154+
155+
export interface UrlResolveKindSymbol {
156+
kind: "symbol";
157+
file: ShortPath;
158+
symbol: string;
159+
}
160+
161+
export type UrlResolveKind =
162+
| UrlResolveKindRoot
163+
| UrlResolveKindAllSymbols
164+
| UrlResolveKindCategory
165+
| UrlResolveKindFile
166+
| UrlResolveKindSymbol;
167+
168+
interface HrefResolver {
169+
/** Resolver for how files should link to eachother. */
170+
resolvePath?(current: UrlResolveKind, target: UrlResolveKind): string;
171+
/** Resolver for global symbols, like the Deno namespace or other built-ins */
172+
resolveGlobalSymbol?(symbol: string[]): string | undefined;
173+
/** Resolver for symbols from non-relative imports */
174+
resolveImportHref?(symbol: string[], src: string): string | undefined;
175+
/** Resolve the URL used in source code link buttons. */
176+
resolveSource?(location: Location): string | undefined;
177+
/**
178+
* Resolve external JSDoc module links.
179+
* Returns a tuple with link and title.
180+
*/
181+
resolveExternalJsdocModule?(
182+
module: string,
183+
symbol?: string,
184+
): { link: string; title: string } | undefined;
185+
}
186+
187+
export interface UsageComposerEntry {
188+
/** Name for the entry. Can be left blank in singleMode. */
189+
name: string;
190+
/** Icon for the entry. */
191+
icon?: string;
192+
}
193+
194+
export type UsageToMd = (
195+
url: string,
196+
customFileIdentifier: string | undefined,
197+
) => string;
198+
199+
export interface UsageComposer {
200+
/** Whether the usage should only display a single item and not have a dropdown. */
201+
singleMode: boolean;
202+
203+
/**
204+
* Composer to generate usage.
205+
*
206+
* @param currentResolve The current resolve.
207+
* @param usageToMd Callback to generate a usage import block.
208+
*/
209+
compose(
210+
currentResolve: UrlResolveKind,
211+
usageToMd: UsageToMd,
212+
): Map<UsageComposerEntry, string>;
213+
}
214+
215+
interface GenerateOptions {
216+
/** The name of the package to use in the breadcrumbs. */
217+
packageName?: string;
218+
/** The main entrypoint if one is present. */
219+
mainEntrypoint?: string;
220+
/** Composer for generating the usage of a symbol of module. */
221+
usageComposer?: UsageComposer;
222+
/** Resolver for how links should be resolved. */
223+
hrefResolver?: HrefResolver;
224+
/** Map for remapping module names to a custom value. */
225+
rewriteMap?: Record<string, string>;
226+
/**
227+
* Map of categories to their markdown description.
228+
* Only usable in category mode (single d.ts file with categories declared).
229+
*/
230+
categoryDocs?: Record<string, string | undefined>;
231+
/** Whether to disable search. */
232+
disableSearch?: boolean;
233+
/**
234+
* Map of modules, where the value is a map of symbols with value of a link to
235+
* where this symbol should redirect to.
236+
*/
237+
symbolRedirectMap?: Record<string, Record<string, string>>;
238+
/**
239+
* Map of modules, where the value is a link to where the default symbol
240+
* should redirect to.
241+
*/
242+
defaultRedirectMap?: Record<string, string>;
243+
/**
244+
* Hook to inject content in the `head` tag.
245+
*
246+
* @param root the path to the root of the output.
247+
*/
248+
headInject?(root: string): string;
249+
/**
250+
* Function to render markdown.
251+
*
252+
* @param md The raw markdown that needs to be rendered.
253+
* @param titleOnly Whether only the title should be rendered. Recommended syntax to keep is:
254+
* - paragraph
255+
* - heading
256+
* - text
257+
* - code
258+
* - html inline
259+
* - emph
260+
* - strong
261+
* - strikethrough
262+
* - superscript
263+
* - link
264+
* - math
265+
* - escaped
266+
* - wiki link
267+
* - underline
268+
* - soft break
269+
* @param filePath The filepath where the rendering is happening.
270+
* @param anchorizer Anchorizer used to generate slugs and the sidebar.
271+
* @return The rendered markdown.
272+
*/
273+
markdownRenderer(
274+
md: string,
275+
titleOnly: boolean,
276+
filePath: ShortPath | undefined,
277+
anchorizer: (content: string, depthLevel: number) => string,
278+
): string | undefined;
279+
/** Function to strip markdown. */
280+
markdownStripper(md: string): string;
281+
}
282+
283+
const defaultUsageComposer: UsageComposer = {
284+
singleMode: true,
285+
compose(currentResolve, usageToMd) {
286+
if ("file" in currentResolve) {
287+
return new Map([[
288+
{ name: "" },
289+
usageToMd(currentResolve.file.specifier, undefined),
290+
]]);
291+
} else {
292+
return new Map();
293+
}
294+
},
295+
};
296+
297+
/**
298+
* Generate HTML files for provided {@linkcode DocNode}s.
299+
* @param options Options for the generation.
300+
* @param docNodesByUrl DocNodes keyed by their absolute URL.
301+
*/
302+
export async function generateHtml(
303+
options: GenerateOptions,
304+
docNodesByUrl: Record<string, Array<DocNode>>,
305+
): Promise<Record<string, string>> {
306+
const {
307+
usageComposer = defaultUsageComposer,
308+
} = options;
309+
310+
const wasm = await instantiate();
311+
return wasm.generate_html(
312+
options.packageName,
313+
options.mainEntrypoint,
314+
usageComposer.singleMode,
315+
usageComposer.compose,
316+
options.rewriteMap,
317+
options.categoryDocs,
318+
options.disableSearch ?? false,
319+
options.symbolRedirectMap,
320+
options.defaultRedirectMap,
321+
options.hrefResolver?.resolvePath,
322+
options.hrefResolver?.resolveGlobalSymbol || (() => undefined),
323+
options.hrefResolver?.resolveImportHref || (() => undefined),
324+
options.hrefResolver?.resolveSource || (() => undefined),
325+
options.hrefResolver?.resolveExternalJsdocModule || (() => undefined),
326+
options.markdownRenderer,
327+
options.markdownStripper,
328+
options.headInject,
329+
docNodesByUrl,
330+
);
331+
}

js/test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
22

33
import { assert, assertEquals, assertRejects } from "jsr:@std/assert@0.223";
4-
import { doc } from "./mod.ts";
4+
import { doc, generateHtml } from "./mod.ts";
55

66
Deno.test({
77
name: "doc()",
@@ -123,3 +123,28 @@ Deno.test({
123123
assertEquals(entries[0].name, "B");
124124
},
125125
});
126+
127+
Deno.test({
128+
name: "generateHtml()",
129+
async fn() {
130+
const entries = await doc(
131+
"https://deno.land/std@0.104.0/fmt/colors.ts",
132+
);
133+
134+
const files = await generateHtml({
135+
markdownRenderer(
136+
md,
137+
_titleOnly,
138+
_filePath,
139+
_anchorizer,
140+
) {
141+
return md;
142+
},
143+
markdownStripper(md: string) {
144+
return md;
145+
},
146+
}, { ["file:///colors.ts"]: entries });
147+
148+
assertEquals(Object.keys(files).length, 61);
149+
},
150+
});

lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ deno_graph = { workspace = true }
1919
deno_doc = { path = "../", default-features = false }
2020
import_map.workspace = true
2121
serde.workspace = true
22+
indexmap = "2.6.0"
2223

2324
console_error_panic_hook = "0.1.7"
2425
js-sys = "=0.3.69"

0 commit comments

Comments
 (0)