Skip to main content

html_generator/
wasm.rs

1// Copyright © 2023 - 2026 HTML Generator. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! WebAssembly bindings.
5//!
6//! Compiled in only when the crate is built with `--features wasm`
7//! and a `wasm32-*` target. Exposes a small JS-friendly surface so
8//! the same Markdown-to-accessible-HTML pipeline that runs server-
9//! side can render directly inside Cloudflare Workers, Vercel Edge,
10//! browser previews, and Node-via-WASI without extra plumbing.
11//!
12//! Three functions are exported:
13//!
14//! * [`generate_html_wasm`] — the simplest entry point. Takes a
15//!   Markdown string, returns the HTML fragment. Uses
16//!   [`crate::HtmlConfig::default`] under the hood (ARIA on, TOC
17//!   off, JSON-LD off, no full-document wrap, no minify).
18//! * [`generate_html_full_document_wasm`] — wraps the output in a
19//!   complete HTML5 document.
20//! * [`generate_html_with_options_wasm`] — accepts a JSON string
21//!   describing the subset of [`crate::HtmlConfig`] flags that make
22//!   sense in a browser/edge runtime (no file I/O, no async).
23//!
24//! Errors are surfaced as JS exceptions: the bindings return
25//! `Result<String, JsValue>` and `wasm-bindgen` materialises the
26//! `Err(JsValue)` as a thrown JS `Error` on the JS side.
27//!
28//! # Examples
29//!
30//! ```ignore
31//! // doctest is `ignore`d because it only compiles when the `wasm`
32//! // feature is on and the target is `wasm32`. The `cargo test`
33//! // target runs on native; the WASM smoke test lives in
34//! // `tests/wasm_smoke.rs` and is driven by `wasm-bindgen-test`.
35//! use wasm_bindgen::prelude::*;
36//! use html_generator::wasm::generate_html_wasm;
37//!
38//! let html: Result<String, JsValue> = generate_html_wasm("# Hi");
39//! assert!(html.unwrap().contains("<h1>"));
40//! ```
41//!
42//! Build for the browser:
43//!
44//! ```text
45//! cargo build --release --target wasm32-unknown-unknown --features wasm
46//! ```
47//!
48//! Or with `wasm-pack` for an npm-publishable bundle:
49//!
50//! ```text
51//! wasm-pack build --target web --features wasm
52//! ```
53
54use wasm_bindgen::prelude::*;
55
56use crate::{generate_html, HtmlConfig};
57
58/// Render Markdown to an accessible HTML fragment.
59///
60/// This is the simplest WASM entry point — no configuration, the
61/// pipeline runs with [`HtmlConfig::default`] (ARIA on, TOC off,
62/// JSON-LD off, no full-document wrap).
63///
64/// # Errors
65///
66/// Returns a `JsValue` carrying the [`crate::error::HtmlError`]
67/// display string when the underlying Markdown render fails.
68/// `wasm-bindgen` materialises this on the JS side as a thrown
69/// JavaScript `Error`.
70#[wasm_bindgen(js_name = generateHtml)]
71pub fn generate_html_wasm(markdown: &str) -> Result<String, JsValue> {
72    generate_html(markdown, &HtmlConfig::default())
73        .map_err(|e| JsValue::from_str(&e.to_string()))
74}
75
76/// Render Markdown wrapped in a full HTML5 document skeleton.
77///
78/// Convenience wrapper that flips `generate_full_document` on
79/// before delegating to [`generate_html`]. The output starts with
80/// `<!DOCTYPE html>` and is suitable for direct write-out from a
81/// Worker / Edge function.
82///
83/// # Errors
84///
85/// Same as [`generate_html_wasm`].
86#[wasm_bindgen(js_name = generateHtmlFullDocument)]
87pub fn generate_html_full_document_wasm(
88    markdown: &str,
89) -> Result<String, JsValue> {
90    let cfg = HtmlConfig {
91        generate_full_document: true,
92        ..HtmlConfig::default()
93    };
94    generate_html(markdown, &cfg)
95        .map_err(|e| JsValue::from_str(&e.to_string()))
96}
97
98/// Render Markdown with a JSON-encoded subset of [`HtmlConfig`].
99///
100/// The JSON object accepts these keys (all optional, all booleans
101/// or strings, defaults match [`HtmlConfig::default`]):
102///
103/// * `add_aria_attributes` *(bool)* — inject ARIA labels and roles.
104/// * `generate_toc` *(bool)* — replace the `[[TOC]]` placeholder
105///   with a generated table of contents.
106/// * `generate_structured_data` *(bool)* — emit a `<script
107///   type="application/ld+json">` JSON-LD block.
108/// * `generate_full_document` *(bool)* — wrap output in
109///   `<!DOCTYPE html>`.
110/// * `enable_math` *(bool)* — render `$..$` and `$$..$$` to MathML
111///   (requires the `math` feature, on by default).
112/// * `enable_diagrams` *(bool)* — rewrite mermaid fenced blocks for
113///   client-side mermaid.js.
114/// * `language` *(string)* — BCP 47 language code for the `<html>`
115///   `lang` attribute when full-document mode is on.
116/// * `minify_output` *(bool)* — minify the final HTML.
117///
118/// File-I/O fields (`encoding`, `max_buffer_size`) are accepted for
119/// shape compatibility but ignored: WASM has no host filesystem.
120///
121/// # Errors
122///
123/// Returns a `JsValue` carrying:
124///
125/// * `"invalid options JSON: …"` if `options_json` does not parse
126///   as a JSON object.
127/// * The [`crate::error::HtmlError`] display string when the
128///   underlying render fails.
129#[wasm_bindgen(js_name = generateHtmlWithOptions)]
130pub fn generate_html_with_options_wasm(
131    markdown: &str,
132    options_json: &str,
133) -> Result<String, JsValue> {
134    let opts: serde_json::Value = serde_json::from_str(options_json)
135        .map_err(|e| {
136            JsValue::from_str(&format!("invalid options JSON: {e}"))
137        })?;
138
139    let mut cfg = HtmlConfig::default();
140    if let Some(b) =
141        opts.get("add_aria_attributes").and_then(|v| v.as_bool())
142    {
143        cfg.add_aria_attributes = b;
144    }
145    if let Some(b) = opts.get("generate_toc").and_then(|v| v.as_bool())
146    {
147        cfg.generate_toc = b;
148    }
149    if let Some(b) = opts
150        .get("generate_structured_data")
151        .and_then(|v| v.as_bool())
152    {
153        cfg.generate_structured_data = b;
154    }
155    if let Some(b) =
156        opts.get("generate_full_document").and_then(|v| v.as_bool())
157    {
158        cfg.generate_full_document = b;
159    }
160    if let Some(b) = opts.get("enable_math").and_then(|v| v.as_bool()) {
161        cfg.enable_math = b;
162    }
163    if let Some(b) =
164        opts.get("enable_diagrams").and_then(|v| v.as_bool())
165    {
166        cfg.enable_diagrams = b;
167    }
168    if let Some(b) = opts.get("minify_output").and_then(|v| v.as_bool())
169    {
170        cfg.minify_output = b;
171    }
172    if let Some(s) = opts.get("language").and_then(|v| v.as_str()) {
173        cfg.language = s.to_string();
174    }
175
176    generate_html(markdown, &cfg)
177        .map_err(|e| JsValue::from_str(&e.to_string()))
178}