Skip to main content

Crate html_generator

Crate html_generator 

Source
Expand description

HTML Generator logo

html-generator

Pure Rust library for transforming Markdown into SEO-optimized, accessible HTML. Zero unsafe code.

Build Crates.io Docs.rs Coverage OpenSSF Best Practices lib.rs


§Contents

Getting started

Library reference

Operational


§Install

[dependencies]
html-generator = "0.0.11"

§Optional async support

[dependencies]
html-generator = { version = "0.0.11", features = ["async"] }

§Build from source

git clone https://github.com/sebastienrousseau/html-generator.git
cd html-generator
make          # check + clippy + test

Requires Rust 1.80.0+. Tested on Linux, macOS, and Windows.


§Requirements

  • Rust 1.80.0 or newer. rust-version in Cargo.toml is the floor and Cargo enforces it; CI builds on stable across Linux, macOS and Windows. See the minimum-toolchain policy for when and why it may move.
  • A std platform, or wasm32 with the wasm feature. The crate uses std unconditionally; there is no no_std build.
  • No async runtime is required. Every entry point is synchronous unless you enable the async feature, which adds a Tokio-based wrapper for callers who already run one.

§Quick Start

use html_generator::{generate_html, HtmlConfig};

fn main() -> Result<(), html_generator::error::HtmlError> {
    let markdown = "# Hello\n\nThis is **bold** text.";
    let config = HtmlConfig::default();
    let html = generate_html(markdown, &config)?;
    println!("{html}");
    Ok(())
}

§Overview

html-generator converts Markdown into production-ready HTML with a configurable pipeline that applies accessibility, SEO, table of contents, math, diagrams, and minification in a single pass. No raw HTML passthrough by default — safe for untrusted input. Runs natively or as WebAssembly in browsers, Cloudflare Workers, and edge runtimes.

  • Full CommonMark with extensions (tables, strikethrough, task lists, superscript)
  • Front matter extraction from YAML (---), TOML (+++), and JSON ({...})
  • WCAG-compliant output with automatic ARIA attribute injection
  • JSON-LD structured data appended for rich search results
  • Table of contents injected at [[TOC]] placeholder
  • Server-side LaTeX → MathML for $..$ and $$..$$ (no client-side JS needed)
  • Mermaid diagram passthrough for \u{60}\u{60}\u{60}mermaid fenced blocks
  • In-memory minification without disk I/O
  • WebAssembly bindings via wasm-bindgen (browsers, Workers, Edge)
  • Optional async via tokio spawn_blocking (behind async feature)
  • Zero unsafe code via #![forbid(unsafe_code)] at crate root
MetricValue
Source~12,900 lines across 11 modules (src/yaml/ is a vendored snapshot, see FAQ)
Test suite533 unit/integration tests + 163 doctests + 4 WASM smoke tests = 700 total
Coverage98.18% line coverage (cargo llvm-cov); Codecov project ≥95%, patch ≥90% gates
Examples14 runnable examples, all executed in CI
Dependencies13 native runtime + 1 optional async (tokio) + 2 optional WASM (wasm-bindgen, js-sys)
MSRVRust 1.80.0
WASM bundle5.8 MB raw / 2.0 MB gzipped (after wasm-opt -Os)
CI gates10 distinct checks including end-to-end wasm-pack test --node against Node 20

§Features

Markdown to HTMLFull CommonMark via mdx-gen with extensions: tables, strikethrough, task lists, autolinks, superscript. Custom class blocks via :::class syntax. Image class attributes via ![alt](url).class="cls".
AccessibilityAutomatic ARIA attribute injection for buttons, navs, forms, inputs, tabs, modals, accordions, tooltips. WCAG 2.1 validation (Levels A, AA, AAA). Heading structure checks. Language attribute validation.
Front matterYAML (---), TOML (+++), JSON ({...}) delimiters. extract_front_matter strips metadata and returns body. extract_front_matter_data parses metadata into serde_json::Value.
Table of contentsgenerate_table_of_contents builds <ul> from headings. Pipeline injects at [[TOC]] placeholder when generate_toc is enabled.
SEOMetaTagsBuilder for meta tag generation. generate_structured_data for JSON-LD <script> output with configurable @type and additional properties. HTML entity escaping via escape_html.
Math (MathML)enable_math flag converts $..$ and $$..$$ LaTeX spans to native <math> MathML via pulldown-latex. Server-side, no JS bundle. Conservative regex matchers leave $5 currency literals alone. Behind the math feature (default-on).
Diagrams (Mermaid)enable_diagrams flag rewrites \u{60}\u{60}\u{60}mermaid fenced blocks to <pre class="mermaid"> for the standard client-side mermaid.js bundle. Diagram source flows through verbatim.
MinificationFile-based minify_html(path) and in-memory minify_html_string(html). Preserves HTML semantics, strips comments, minifies CSS/JS. Configurable via MinifyConfig.
WebAssemblywasm feature exposes generateHtml, generateHtmlFullDocument, generateHtmlWithOptions to JavaScript via wasm-bindgen. Build with wasm-pack build --target web --features wasm --no-default-features.
PerformanceRegexes and CSS selectors compiled once into static Lazy. SIMD-backed str::contains short-circuits before any html5ever parse. DOM-aware element replacement handles attribute reordering. 2.09 ms full pipeline on an 8 KB blog payload (comrak parse alone is 172 µs).
AsyncOptional async feature enables async_generate_html via tokio spawn_blocking. Synchronous users pay zero cost — tokio not compiled without the feature.
Security#![forbid(unsafe_code)]. Raw HTML stripped by default (allow_unsafe_html: false). All user-controlled attributes escaped. NUL-byte rejection on file paths. Directory traversal blocked. Input size limits enforced.

§Library Usage

Full pipeline
use html_generator::{generate_html, HtmlConfig};

let config = HtmlConfig {
    add_aria_attributes: true,
    generate_toc: true,
    generate_structured_data: true,
    minify_output: true,
    ..HtmlConfig::default()
};

let markdown = "[[TOC]]\n\n# Introduction\n\nWelcome to the guide.\n\n## Getting Started\n\nFollow these steps.";
let html = generate_html(markdown, &config)?;
// Output includes: ARIA attributes, TOC at [[TOC]], JSON-LD, minified

The pipeline applies steps in order:

  1. Markdown → HTML (with extensions)
  2. Accessibility (ARIA attributes)
  3. Table of contents (inject at [[TOC]])
  4. Structured data (append JSON-LD)
  5. Minification (compress)
Front matter
use html_generator::utils::extract_front_matter_data;

// YAML front matter
let content = "---\ntitle: My Page\nauthor: Jane Doe\n---\n# Hello";
let (metadata, body) = extract_front_matter_data(content)?;
assert_eq!(metadata["title"], "My Page");
assert_eq!(body, "# Hello");

// TOML front matter
let content = "+++\ntitle = \"My Page\"\nauthor = \"Jane Doe\"\n+++\n# Hello";
let (metadata, body) = extract_front_matter_data(content)?;
assert_eq!(metadata["title"], "My Page");

// JSON front matter
let content = "{\"title\": \"My Page\"}\n# Hello";
let (metadata, body) = extract_front_matter_data(content)?;
assert_eq!(metadata["title"], "My Page");
Table of contents
use html_generator::{generate_html, HtmlConfig};

let markdown = "[[TOC]]\n\n# Chapter 1\n\n## Section 1.1\n\n# Chapter 2";
let config = HtmlConfig {
    generate_toc: true,
    ..HtmlConfig::default()
};
let html = generate_html(markdown, &config)?;
assert!(html.contains(r#"<ul>"#));
assert!(html.contains(r#"<a href="\#chapter-1">"#));
SEO and structured data
use html_generator::seo::{MetaTagsBuilder, generate_structured_data, StructuredDataConfig};
use std::collections::HashMap;

// Meta tags
let meta = MetaTagsBuilder::new()
    .with_title("My Page")
    .with_description("A great page")
    .add_meta_tag("author", "Jane Doe")
    .build()?;

// JSON-LD structured data
let html = r#"<html><head><title>My Page</title></head><body><p>Content</p></body></html>"#;
let config = StructuredDataConfig {
    page_type: "Article".to_string(),
    additional_data: Some(HashMap::from([("author".to_string(), "Jane".to_string())])),
    ..Default::default()
};
let json_ld = generate_structured_data(html, Some(config))?;
assert!(json_ld.contains("application/ld+json"));
Accessibility
use html_generator::accessibility::{add_aria_attributes, validate_wcag, AccessibilityConfig};

let html = r#"<button>Submit</button><nav><ul><li>Home</li></ul></nav>"#;

// Enhance with ARIA attributes
let enhanced = add_aria_attributes(html, None)?;
assert!(enhanced.contains("aria-label"));

// Validate WCAG compliance
let config = AccessibilityConfig::default();
let report = validate_wcag(&enhanced, &config, None)?;
println!("Issues found: {}", report.issue_count);
Minification
use html_generator::performance::minify_html_string;

let html = "<html>  <body>  <p>Hello</p>  </body>  </html>";
let minified = minify_html_string(html)?;
assert_eq!(minified, "<html><body><p>Hello</p></body></html>");
Diagnostics

The default generate_html silently degrades when optional steps fail. Use generate_html_with_diagnostics to inspect which steps succeeded:

use html_generator::{generate_html_with_diagnostics, HtmlConfig};

let config = HtmlConfig {
    add_aria_attributes: true,
    generate_toc: true,
    generate_structured_data: true,
    minify_output: true,
    ..HtmlConfig::default()
};

let output = generate_html_with_diagnostics("# Hello", &config)?;
println!("HTML: {} bytes", output.html.len());
for d in &output.diagnostics {
    eprintln!("warning: {d}");
}
Async (optional)

Enable with features = ["async"]:

use html_generator::performance::async_generate_html;

#[tokio::main]
async fn main() -> Result<(), html_generator::error::HtmlError> {
    let html = async_generate_html("# Hello\n\nWorld").await?;
    println!("{html}");
    Ok(())
}

§Configuration

use html_generator::HtmlConfig;

let config = HtmlConfig {
    enable_syntax_highlighting: true,       // Syntax-highlighted code blocks
    syntax_theme: Some("github".into()),    // Highlighting theme
    minify_output: false,                   // Compress output HTML
    add_aria_attributes: true,              // Inject ARIA attributes
    generate_structured_data: false,        // Append JSON-LD
    generate_toc: false,                    // Inject TOC at [[TOC]]
    allow_unsafe_html: false,               // Strip raw HTML (XSS-safe default)
    sanitize_html: false,                   // Sanitize via ammonia (when unsafe is on)
    generate_full_document: false,          // Wrap in HTML5 boilerplate
    max_input_size: 5 * 1024 * 1024,        // 5MB input limit
    max_buffer_size: 16 * 1024 * 1024,      // 16MB I/O buffer
    language: "en-GB".into(),               // Content language (used in html lang attr)
    encoding: "utf-8".into(),               // File I/O encoding
    enable_math: false,                     // LaTeX → MathML for $..$ / $$..$$
    enable_diagrams: false,                 // Mermaid passthrough for ```mermaid blocks
};

Use the builder for validated configuration:

use html_generator::HtmlConfig;

let config = HtmlConfig::builder()
    .with_syntax_highlighting(true, Some("monokai".into()))
    .with_language("en-US")
    .build()?;

§Examples

ExampleDescription
helloHeading, lists, code blocks, links — basic Markdown to HTML
pipelineFull pipeline: ARIA + TOC + JSON-LD + minification in one pass
frontmatterYAML, TOML, JSON front matter extraction and parsing
accessibilityARIA injection for buttons, navs, forms; WCAG validation
seoMeta tags, JSON-LD structured data, HTML entity escaping
tocTable of contents from headings, [[TOC]] placeholder
minifyIn-memory HTML minification with size savings
errorsError variants, type matching, graceful recovery patterns
configHtmlConfig builder, validation, field inspection
headersCustom ID and class generators for heading elements
custom_syntaxTriple-colon blocks (:::warning) and image classes
emojisBundled emoji data, emoji-to-ARIA-label mapping
math_and_diagramsLaTeX → MathML and \u{60}\u{60}\u{60}mermaid passthrough
asyncAsynchronous generation via tokio (requires --features async)

Run any of them with cargo run --example <name>, or all fourteen with make examples. CI does the same on every push, so an example that stops working fails the build.

cargo run --example hello
cargo run --example async --features async

§Benchmarks

Comparative throughput on the same realistic 8 KB blog payload, measured with Criterion (--quick) on an Apple M-series CPU with [profile.bench] at opt-level = 3 and fat LTO. Numbers are worth nothing without the host they came from; reproduce them on yours with cargo bench --bench competitors.

EngineTime / iterWhat it does
pulldown_cmark (parse only)45 µsPull-parser, no post-processing. Fastest plain CommonMark in Rust.
comrak (parse only)172 µsThe CommonMark/GFM parser this crate wraps.
html_generator (full pipeline)2.09 msParse + ARIA injection + TOC + JSON-LD + minification.

Pure parsers will always be faster — they don’t do ARIA, JSON-LD, TOC, or minification. html-generator does all four in one pass; the ~2 ms overhead is what buys WCAG-compliant output without a downstream post-processing layer. Reproduce with:

cargo bench --bench competitors

§Math and diagrams

Two opt-in post-processors turn ordinary Markdown into rich technical documentation without client-side JavaScript for math:

use html_generator::{generate_html, HtmlConfig};

let md = r"

In a right triangle, $a^2 + b^2 = c^2$.

```mermaid
graph LR
    A --> B
```";
let cfg = HtmlConfig {
    enable_math: true,        // $..$ and $$..$$ → <math> MathML
    enable_diagrams: true,    // ```mermaid → <pre class="mermaid">
    ..HtmlConfig::default()
};
let html = generate_html(md, &cfg)?;
  • Math — server-side LaTeX → MathML via pulldown-latex (gated behind the math feature, on by default). Browsers render MathML natively, so no client-side bundle is required. Parse errors are encoded inline as <merror> markers rather than crashing the build.
  • Diagrams\u{60}\u{60}\u{60}mermaid fenced blocks become <pre class="mermaid">…</pre> so the standard mermaid.js loader picks them up. Drop a single <script type="module">import mermaid from "https://…/mermaid.esm.mjs"; mermaid.initialize({startOnLoad:true});</script> in your page and you’re done.

§WebAssembly

The same pipeline runs in Cloudflare Workers, Vercel Edge, browsers, and Node — without changing API:

cargo build --release --target wasm32-unknown-unknown \
  --features wasm --no-default-features
# or, to publish an npm bundle:
wasm-pack build --target web --features wasm --no-default-features

Three JS-friendly entry points are exposed via wasm-bindgen:

JS nameDescription
generateHtml(markdown)Render Markdown to an accessible HTML fragment with default config.
generateHtmlFullDocument(markdown)Same but wrapped in <!DOCTYPE html><html>…</html>.
generateHtmlWithOptions(markdown, optionsJson)Pass a JSON object configuring add_aria_attributes, generate_toc, enable_math, enable_diagrams, etc.

WASM builds drop mdx-gen’s :::class, image-class, and syntect syntax highlighting (the underlying tokio/onig C dependencies do not compile to wasm32-unknown-unknown); CommonMark + GFM (tables, strikethrough, autolinks, tasklists, superscript) plus the full ARIA / TOC / JSON-LD / math / mermaid post-processing layer renders identically.

Use it from JavaScript:

// pkg/ generated by `wasm-pack build --target web ...`
import init, {
  generateHtml,
  generateHtmlWithOptions,
} from "./pkg/html_generator.js";

await init();

// Simple render with defaults (ARIA on):
const fragment = generateHtml("# Hello, **world**!");

// Render with custom options:
const article = generateHtmlWithOptions(
  "# Math\n\n$$E = mc^2$$",
  JSON.stringify({
    enable_math: true,
    generate_full_document: true,
    language: "en-GB",
  }),
);

From Cloudflare Workers / Vercel Edge: use wasm-pack build --target bundler and import the generated module from your worker entry point. The JS-side API is identical to the browser case.

§Bundle size

Measured wasm-pack build --release --target web output, post wasm-opt -Os:

Feature set.wasm raw.wasm gzipped
--features wasm,math5.8 MB2.0 MB
--features wasm (no math)5.7 MB1.96 MB

The math feature adds ~40 KB gzipped. Both bundles fit comfortably in Cloudflare Workers’ paid plan (10 MB compressed); the free plan (1 MB compressed) requires further trimming — the ammonia, minify-html, and scraper-on-html5ever deps account for the bulk of the binary.

Smoke tests live in tests/wasm_smoke.rs and run under wasm-pack test --node --no-default-features --features wasm,math. The CI’s wasm-build job exercises this exact command on every push.


§When not to use html-generator

Cases where something else fits better, listed because the honest answer is “not yet” or “by design” rather than a disagreement about priorities.

  • You only need Markdown to HTML. Use comrak or pulldown-cmark directly. They are the parsers underneath, they are an order of magnitude faster, and everything this crate adds on top is overhead you would not be using.
  • You need no_std. The crate uses std unconditionally. The wasm32 build is the only non-native target it supports, and it still needs std.
  • You need a full HTML parser’s error recovery. The accessibility and SEO passes read the generated document with scraper; they are built for markup this crate produced, not for arbitrary broken HTML from the wild.
  • You need WCAG conformance as a legal guarantee. validate_wcag checks the rules it implements: heading order, image alt text, form labels, landmark structure, link text. Conformance is a property of a whole site and its content, and no library can certify it for you.
  • You want raw HTML in Markdown to pass through untouched by default. It does not, and that is deliberate. See Security.

§Development

make              # check + clippy + test
make test         # all tests, all features
make clippy       # lints, warnings denied
make fmt          # formatting check
make lint         # markdownlint + codespell + REUSE
make doc          # rustdoc with warnings denied
make coverage     # line coverage gate (98%, excluding src/wasm.rs)
make miri         # lib tests under Miri
make fuzz         # build every target, replay corpus and regressions
make examples     # run all fourteen examples
make bench-smoke  # compile and run each bench once
make versions     # every version-bearing file agrees
make deny / vet / audit   # supply chain

DEVELOPMENT.md maps each CI job to its local equivalent and explains the gotchas.

§Fuzzing

Three cargo-fuzz targets live under fuzz/fuzz_targets/:

cargo +nightly fuzz run fuzz_markdown       # the whole pipeline, every step on
cargo +nightly fuzz run fuzz_front_matter   # extraction
cargo +nightly fuzz run fuzz_accessibility  # ARIA enrichment + WCAG validation

fuzz_accessibility is the one that matters most: the enrichment pass rewrites markup by byte offset, which is where this crate’s slice-boundary bugs have lived. fuzz/corpus/<target> holds the committed seeds and fuzz/regressions/<target> every fixed-bug input; both replay on each push, so a fixed crash cannot silently return.

cargo-fuzz must be installed from source (cargo install --locked cargo-fuzz): the prebuilt binary is a musl build and infers its own build triple as the fuzz target.

§Miri

The crate is #![forbid(unsafe_code)], so Miri does not police its own code. The job exists to check the interaction with dependencies that do use unsafe internally.

make miri     # cargo +nightly miri test --lib

§CI

WorkflowTriggerPurpose
ci.ymlpush, PRclippy, fmt, tests across three OSes, coverage, audit
quality.ymlpush, PRcoverage gate, Miri, fuzz replay, docs lint, cargo-vet ratchet, release hygiene
docs.ymlpush to mainbuild and deploy API docs to GitHub Pages
release.ymltag v*validate, build, GitHub Release

See CONTRIBUTING.md for signed commits and PR guidelines.


§Security

Reporting: never open a public issue for a vulnerability. See SECURITY.md for the private channel and disclosure policy.

This crate turns untrusted Markdown into HTML a site will serve, so injection is the first-order risk and the reason for most of what follows.

§Injection

Raw HTML in Markdown is off by default: allow_unsafe_html is false, so embedded markup is escaped and nothing a document author writes reaches the page as live HTML. sanitize_html is a separate switch, also off by default, and has no effect on its own: it runs the output through ammonia, an allow-list sanitiser, only when allow_unsafe_html is true. If you enable one, enable both. Every user-controlled attribute value is escaped on the way out.

§Architectural posture

  • #![forbid(unsafe_code)] — the compiler proves the absence of unsafe blocks.
  • No C dependencies, no FFI, no network I/O. File access happens only where the caller names a path, and .. traversal is rejected.
  • Input size limits at every boundary: a per-call max_input_size (5 MiB by default), a 1 MB cap on the HTML the accessibility and SEO passes will rewrite, and a 16 MiB reader buffer.

§Supply chain

  • cargo-deny and cargo-audit in CI; documented advisory exemptions live in .cargo/audit.toml with the upstream reason for each.
  • cargo-vet provenance in supply-chain/, with an exemption baseline the CI ratchet cannot exceed.
  • noyalib pinned exactly (=0.0.X); a bump is a deliberate release.
  • Cargo.lock committed; CI builds --locked. Actions pinned by SHA.
  • REUSE 3.3 compliant, linted in CI.
  • Commits on main are signed; releases are signed tags (KEYS.asc).

§Documentation

The four entry points, identical across every repo in the family:

DocumentCovers
CHANGELOG.mdper-release notes, Keep a Changelog format
SECURITY.mddisclosure policy, injection posture, resource limits, supply chain
CONTRIBUTING.mdbranch and commit conventions, PR expectations, code standards
GOVERNANCE.mdwho decides what, how changes land
SUPPORT.mdwhere to ask, what to expect
AGENTS.mdinvariants for AI-assisted contributions

§Stability guarantees

  • Versioning. SemVer, with the pre-1.0 posture that the patch number is the breaking axis during 0.0.x. Releases increment by +0.0.1 and every breaking change is called out in CHANGELOG.md.
  • Output stability. For a generator, output is API: a change to the HTML produced for a given Markdown input — the ARIA attributes added, the heading ids emitted, the JSON-LD shape, what the minifier collapses — is treated as breaking even when no Rust signature moves.
  • Determinism. The same Markdown and the same HtmlConfig produce byte-identical HTML on every run and every platform. Anything else is a bug, not a tolerance.
  • Deprecations live for at least two releases with a #[deprecated] note naming the replacement before removal.
  • Version-bearing files are checked against the manifest by scripts/verify-release-versions.sh before a tag exists, so an install snippet cannot go stale.

§Minimum-toolchain policy

The floor is Rust 1.80.0, declared as rust-version in Cargo.toml so Cargo refuses older toolchains with a clear message.

  • When it may rise: only on a release, never silently, and always with the reason in the changelog entry.
  • Why it is where it is: the floor follows the highest requirement in the dependency graph, not an aspiration. It moves when a dependency the crate needs moves it.
  • What is verified: CI builds and tests on stable. The floor is the version Cargo enforces from the manifest.

No claim is made about distro-LTS toolchains. Making one would require a table mapping current distro versions to this floor, and an aspirational claim there is worse than none.


§License

Dual-licensed under Apache 2.0 or MIT, at your option.

Back to Top

Re-exports§

pub use crate::error::HtmlError;
pub use accessibility::add_aria_attributes;
pub use accessibility::validate_wcag;
pub use emojis::load_emoji_sequences;
pub use generator::generate_html;
pub use generator::generate_html_with_diagnostics;
pub use generator::Diagnostic;
pub use generator::DiagnosticLevel;
pub use generator::HtmlOutput;
pub use performance::async_generate_html;
pub use performance::minify_html;
pub use performance::minify_html_string;
pub use seo::generate_meta_tags;
pub use seo::generate_structured_data;
pub use utils::extract_front_matter;
pub use utils::extract_front_matter_data;
pub use utils::format_header_with_id_class;

Modules§

accessibility
Accessibility-related functionality for HTML processing.
constants
Common constants used throughout the library.
elements
HTML5 semantic element builders.
emojis
Emoji Sequences Loader
error
Error types for HTML generation and processing.
generator
HTML generation module for converting Markdown to HTML.
math
Server-side LaTeX → MathML and Mermaid diagram passthrough.
performance
Performance optimization functionality for HTML processing.
seo
Search Engine Optimization (SEO) functionality for HTML processing.
utils
Utility functions for HTML and Markdown processing.
wasm
WebAssembly bindings.

Structs§

HtmlConfig
Configuration options for HTML generation.
HtmlConfigBuilder
Builder for constructing HtmlConfig instances.
MarkdownConfigDeprecated
Legacy configuration type — use HtmlConfig directly instead.

Enums§

ConfigError
Errors that can occur during configuration.
OutputDestination
Output destination for HTML generation.

Functions§

markdown_file_to_html
Converts a Markdown file to HTML.
markdown_to_html
Converts Markdown content to HTML.
validate_language_code
Validates that a language code matches the BCP 47 format (e.g., “en-GB”).

Type Aliases§

Result
Result type alias for library operations.