winged-rust 1.0.1

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

All notable changes to this project are documented here.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.1] - 2026-09-21

### Added

- **The Winged-Swift test suite is fully ported.** All 183 non-skipped cases across 23
  suites now have a Rust counterpart, each naming the Swift case it came from, and
  `scripts/check-swift-parity.sh` checks those names against the Swift source rather than
  trusting them. It is step 7 of `scripts/verify.sh`, and skips itself when the sibling
  checkout is absent. ([#30]https://github.com/micheltlutz/winged-rust/issues/30)

### Fixed

- `seo::common` emitted `<meta name="keywords" content="">` when given an empty list, where
  Winged-Swift omits the tag. `SeoBuilder` already guarded against it, so the empty tag was
  only reachable by calling `common` directly — the guard now lives in `common`, where both
  entry points get it. Found by porting `SEOTests.testCommonOmitsEmptyKeywords`.

- **Rendering no longer recurses**, so nesting depth is no longer bounded by the stack.
  The writer walks an explicit work stack, and `Element` tears its subtree down the same
  way. A 100,000-level tree renders in both modes and is freed without aborting; output is
  byte-identical to the recursive writer for every test and all four golden fixtures.
  A stack overflow is an abort rather than a catchable panic, so this was a
  denial-of-service vector wherever nesting depth could be influenced by untrusted input.
  ([#33]https://github.com/micheltlutz/winged-rust/issues/33)
- `Render for Element` no longer clones the whole subtree on every render. It built a
  `Node::Element(self.clone())` first, which deep-copied the tree — and `Clone` is itself
  recursive, so a deep tree aborted there before the writer ever saw it.
- `RenderOptions::write_indent` returns immediately for an empty indent instead of looping
  once per level to append nothing. That loop is quadratic in depth: it cost 135 seconds on
  a 100,000-level tree, against 0.08 with the early return.

### Changed

- `Element` implements `Drop`. Its fields are private, so no partial move was possible on
  it; a bare `Node::Fragment` chain with no element in it is still freed recursively,
  because giving `Node` a `Drop` would forbid `match node { Node::Element(e) => e }`.

## [1.0.0] - 2026-09-18

### Added

- **Core engine**`Node`, `Element`, `Attribute`, the `Render` trait and `RenderOptions`.
  Rendering is buffered: the whole tree writes into one `String`.
- **Three escapers**`escape_text`, `escape_attribute` and `escape_xml`. Escaping is
  applied once, when content enters the tree.
- **93 HTML elements**, generated from a single `define_elements!` table, plus typed
  constructors for the shapes that carry required arguments (`image`, `iframe_titled`,
  `label_for`, `link_to`, `stylesheet`, `button_typed`, `input_named`, `script_src`).
- **`html!` macro** — nested markup syntax with attributes, `@if` / `@else` and `@for`.
  Expands to the same tree the builder API produces.
- **`Document`** and the **`Layout`** trait.
- **SEO**`open_graph`, `open_graph_article`, `twitter_card`, `common`, and `SeoBuilder`.
- **Accessibility** — a `Role` enum and `audit`, which reports images without `alt`,
  buttons and links with no accessible name, iframes without a title, and inputs with
  nothing a label can attach to.
- **`sitemap`** and **`feed`** — XML sitemaps, sitemap indexes and RSS 2.0.
- **`ssg`**`StaticSiteGenerator` with atomic writes, behind `feature = "ssg"`.
- **`parallel`**`rayon`-backed bulk page generation, behind `feature = "parallel"`.
- **`wasm`**`wasm-bindgen` bindings wrapping the real tree, behind `feature = "wasm"`.
- **`Node::Comment`** — an HTML comment node, which Winged-Swift does not have. `--` in the
  content is neutralised so a comment cannot close early.
- **Golden-file tests** reproducing all four Winged-Swift fixtures byte for byte, with
  `WINGED_UPDATE_FIXTURES=1` to regenerate.
- **NPM package**`web`, `nodejs` and `bundler` builds, 20 KB gzipped, with TypeScript
  typings. `npm/smoke.mjs` renders the golden marketing page through WebAssembly and diffs
  it against the same fixture the Rust tests use, so native and WASM output cannot drift
  apart unnoticed.
- **Browser and Node examples**, both runnable with no bundler.
- **`scripts/build-wasm.sh`** — all three targets, with a gzipped size budget enforced in
  CI.
- **`scripts/verify-npm.sh`**`tsc --noEmit` over the generated typings, then installs a
  packed tarball into a throwaway project rather than linking the workspace, which is what
  catches a wrong `files` allowlist or a broken `exports` map.
- **`scripts/verify.sh`** — one command that is a superset of CI.
- **`scripts/generate-tag-catalog.sh --check`** — documentation drift fails the build.

### Changed from Winged-Swift 2.0.0

Every deliberate difference is listed with its reason in [`PORTING.md`](PORTING.md).
The short version:

- `data_attrs` and `aria_attrs` produce deterministic output; the Swift versions iterate a
  `Dictionary` and do not.
- The node tree is `Send + Sync`. Winged-Swift's `ROADMAP.md` lists its tree not being
  `Sendable` as an unresolved 3.0 problem.
- `seo::common` emits the `<title>` it is given. `SEO.common` accepts a title and drops it.
- `StaticSiteGenerator::clean` refuses empty, root and `..`-containing paths, and page
  paths cannot escape the output directory.
- `generate_multiple` reports every failure rather than the first.
- Writes are atomic.
- The process-wide `HTMLTag.xhtmlSelfClosing` switch is not ported; pass
  `RenderOptions::with_xhtml_self_closing` instead.

### Known limitations

- **Rendering depth is bounded by the stack.** The renderer recurses once per nesting
  level. 256 levels is guaranteed and covered by a test — deeper than browsers themselves
  render; around 2,000 levels aborts the process, and a stack overflow is an abort rather
  than a catchable panic. Only reachable when nesting depth can be influenced by untrusted
  input; documented in `SECURITY.md`, the `README`, and on the `Render` trait. Tracked in
  [#33]https://github.com/micheltlutz/winged-rust/issues/33.

### Fixed relative to Winged-Swift

- `Scripts/generate-tag-catalog.sh` matches `public class [A-Za-z]+: HTMLTag`, which rejects
  digits, so `H1``H6` are silently missing from its checked-in catalog. The Rust generator
  covers all 93 tags, and a test asserts the headings are present.