winged-rust 1.0.0

Fast, type-safe HTML DSL and static site generation engine in Rust and WebAssembly
Documentation

winged-rust

CI License: MIT Rust 1.85+ WebAssembly

Fast, type-safe HTML generation for Rust — and for the browser, through WebAssembly.

A port of Winged-Swift 2.0.0: the same composite tree, the same escape-by-default guarantee, the same compact and pretty render modes. Verified against the Swift project's own golden fixtures, byte for byte.

use winged_rust::prelude::*;
use winged_rust::Document;

let page = Document::new(Some("pt-BR"))
    .head_children([
        meta().attr("charset", "UTF-8"),
        title().text("RideKeeper — track every service"),
        stylesheet("/css/style.css"),
    ])
    .body_children([
        header().child(nav().child(link_to("/").add_class("logo").text("RideKeeper"))),
        main_tag().child(
            article()
                .add_class("card p-4")
                .child(h1().text("Track every service"))
                .child(p().text("Fuel, tyres & chain — all in one place.")),
        ),
    ]);

println!("{}", page.render());

Or with the html! macro:

use winged_rust::{html, prelude::*};

let items = ["Fuel", "Tyres", "Chain"];
let markup = html! {
    section(class = "log") {
        h2 { "Service log" }
        ul { @for item in items { li { (item) } } }
    }
};

Why

Escaping you cannot forget. Text and attribute values are escaped when they enter the tree, not when it is rendered — so nothing is escaped twice and nothing is missed. raw_text is the one, obvious, greppable escape hatch.

# use winged_rust::prelude::*;
assert_eq!(p().text("<script>alert(1)</script>").render(),
           "<p>&lt;script&gt;alert(1)&lt;/script&gt;</p>");

No runtime, no DOM, no allocations you did not ask for. The renderer writes the whole tree into one buffer. There is no template parsing step and no reflection.

It runs in the browser. The same tree, the same renderer, compiled to wasm32-unknown-unknown.

The tree is Send + Sync. Winged-Swift's HTMLTag is a mutable class, which is why a Swift tag tree is not Sendable and why its ROADMAP.md lists that as an unresolved 3.0 problem. Node is a value type, so bulk generation parallelises safely — that is what the parallel feature is built on.

Install

[dependencies]
winged-rust = "1.0"

Features:

feature default what it does
ssg StaticSiteGenerator — writes pages and assets to disk
wasm wasm-bindgen bindings for JavaScript
parallel rayon-backed bulk page generation

What is in it

  • 93 HTML elements, generated from one table — see docs/tag-catalog.md
  • Fluent builder APIset_id, add_class, add_classes, set_style, attr, data_attr(s), aria_attr(s), set_role, child, text, raw_text
  • html! macro — nesting, attributes, @if / @else, @for
  • Document — owns the doctype, renders pretty by default
  • Layout trait — reusable page shells
  • SEO — Open Graph, Twitter Cards, the common <meta> block, and a SeoBuilder
  • Accessibility — ARIA helpers plus an audit that finds images without alt, buttons with no accessible name, iframes without a title
  • Sitemaps and RSS 2.0
  • Static site generation — atomic writes, asset copying, optional parallelism
  • WebAssembly — the same API from JavaScript

WebAssembly

wasm-pack build --target web --features wasm
import init, { element, WDocument } from "./pkg/winged_rust.js";
await init();

const page = new WDocument("pt-BR")
  .addBody(element("h1").text("Track every service"))
  .addBody(element("p").addClass("lead").text("Fuel, tyres & chain."));

document.body.innerHTML = page.render();

The bindings wrap the real tree rather than accumulating pre-rendered strings, so renderPretty() works and JavaScript drives exactly the renderer Rust does.

Parity with Winged-Swift

The Swift project ships four golden fixtures. This crate reproduces all four byte for byte, which is what turns "a port" into a proven port:

fixture what it exercises
marketing-pretty.html doctype, lang, 17 meta tags, tables, boolean attributes, forms, <pre><code>, entity escaping
marketing-compact.html the same tree, minified
sitemap.xml XML escaping, float formatting
feed.xml RSS 2.0 with atom:self

Run WINGED_UPDATE_FIXTURES=1 cargo test --test golden to regenerate them. A regenerated fixture in a pull-request diff is the signal that markup changed.

Deliberate differences from the Swift API — and the reason for each — are listed in PORTING.md.

Known limitations

Rendering depth is bounded by the stack. The renderer recurses once per nesting level, so 256 levels is the guaranteed depth — deeper than browsers themselves render. Around 2,000 levels aborts the process, and a stack overflow is an abort rather than a catchable panic.

This is only reachable if nesting depth can be influenced by untrusted input. If it can, bound the depth before building the tree or render on a thread with a larger stack. See SECURITY.md and #33.

Development

./scripts/verify.sh        # build, test, clippy, fmt, catalog, fixtures, smoke test
cargo test                 # unit, integration and doc tests
cargo bench                # render performance

verify.sh is a superset of CI. Run it before reporting that a change is done.

Contributing

See CONTRIBUTING.md, and AGENTS.md if you are an agent.

License

MIT — see LICENSE.

Original Swift implementation by Michel Anderson Lutz Teixeira.