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}