> 🇧🇷 Tradução para português do [README em inglês](../../README.md), que é o texto de
> referência. Os demais documentos (`SECURITY.md`, `MIGRATION.md`, `CHANGELOG.md`,
> `CONTRIBUTING.md`) existem só em inglês.
<p align="center">
<img src="https://raw.githubusercontent.com/micheltlutz/molt/main/molt-logo.png" alt="Molt" width="220">
</p>
# Molt
> **Uma DSL de HTML segura e sem dependências para Rust e WebAssembly.**
[](https://crates.io/crates/molt-framework)
[](https://docs.rs/molt-framework)
[](https://github.com/micheltlutz/molt/actions/workflows/ci.yml)
[](../../LICENSE)
[](https://www.rust-lang.org)
[](#webassembly)
[](https://github.com/sponsors/micheltlutz)
📦 **[molt-framework no crates.io](https://crates.io/crates/molt-framework)** · 📚 [docs.rs](https://docs.rs/molt-framework) · 🚀 [Primeiros passos](GETTING_STARTED.md)
Molt é uma DSL embutida: você descreve a página em Rust, com um builder fluente ou com a macro
`html!`, e o compilador a verifica. O resultado é uma árvore de valores simples, renderizada
uma única vez como uma **string**, compacta ou indentada. Não há DOM, runtime, linguagem de
template nem reflexão. Por isso não existe nada para interpretar em tempo de execução e nada
para manter sincronizado.
Molt é só a camada de HTML: a árvore de elementos, o renderizador, metadados, uma auditoria de
acessibilidade, sitemaps, feeds, geração de site estático e uma build WebAssembly. Roteamento,
servidores e Markdown ficam com as crates que já fazem isso bem, e se conectam por meio de uma
`String`.
```rust
use molt::prelude::*;
use molt::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());
```
Ou com a macro `html!`, que se expande nas mesmas chamadas do builder, nunca em um segundo
caminho de código:
```rust
use molt::{html, prelude::*};
let items = ["Fuel", "Tyres", "Chain"];
let markup = html! {
section(class = "log") {
h2 { "Service log" }
ul { @for item in items { li { (item) } } }
}
};
```
## Por que Molt
**Seguro por construção.** A garantia central é que o texto e os valores de atributo que você
passa nunca viram marcação, e ela é aplicada no ponto em que o dado entra, não onde você se
lembra de aplicá-la.
- Texto e valores de atributo são escapados uma única vez, na entrada. Não há como esquecer
nem como escapar duas vezes.
- Nomes de tag e de atributo são validados na hora de escrever. Um nome inválido é descartado,
nunca "consertado".
- Atributos de URL (`href`, `src`, `action`, `srcset`, …) que resolvem para `javascript:`,
`vbscript:` ou `data:text/html` são escritos como `#`, inclusive quando disfarçados com
referências de caractere. `srcdoc` nunca é escrito com um valor que você não garantiu.
- JSON dentro de `<script>` passa por um escapador próprio, então um `</script>` em um valor
não consegue fechar o elemento.
- `raw_text`, `Node::raw`, `Attribute::raw` e `trusted_url` são as únicas formas de contornar
qualquer uma dessas proteções. Têm nomes próprios para que uma auditoria seja um único
`grep`.
- `unsafe_code = "forbid"`, `cargo deny` sobre o grafo de dependências, quatro alvos de fuzzing
e um [`SECURITY.md`](../../SECURITY.md) (em inglês) que mapeia cada garantia no OWASP Top 10.
```rust
# use molt::prelude::*;
assert_eq!(p().text("<script>alert(1)</script>").render(),
"<p><script>alert(1)</script></p>");
assert_eq!(a().attr("href", "javascript:alert(1)").render(), r##"<a href="#"></a>"##);
```
**Robusto diante de entrada hostil.** Renderizar, clonar, comparar, auditar e descartar uma
árvore usam sempre uma pilha explícita, nunca a pilha de chamadas. Assim, a profundidade do
aninhamento não consegue derrubar o processo. Uma árvore de 100.000 níveis é renderizada nos
dois modos na suíte de testes, e `RenderOptions::with_max_depth` limita a profundidade que vem
de fora. A saída é fixada byte a byte por fixtures *golden*: uma mudança na marcação é um teste
que falha, nunca uma surpresa em produção. A crate está na versão 1.x, então o que compila
hoje continua compilando, e o [`MIGRATION.md`](../../MIGRATION.md) (em inglês) lista toda
mudança que já alterou a saída.
**Leve.** Zero dependências obrigatórias: `cargo tree` com as features padrão mostra uma única
crate, esta. `wasm-bindgen` e `rayon` são features opcionais. A build WebAssembly tem cerca de
25 KB comprimida com gzip. O renderizador escreve a árvore inteira em um único buffer, o número
de alocações é verificado no CI, e `Node` é `Send + Sync`, então a geração em lote pode ser
paralelizada sem cerimônia.
**Rápido o bastante para não ser o gargalo.** Medido na máquina de desenvolvimento com
`cargo bench --bench render` (Criterion, `opt-level = 3`):
| benchmark | mediana |
| --- | --- |
| renderização compacta, lista de 10.000 itens | ≈ 400 µs |
| documento completo com 50 cards, indentado / compacto | ≈ 8 µs / 6,5 µs |
Montar a árvore custa mais do que renderizá-la; os dois custam muito menos do que a requisição
que pediu a página.
## Instalação
A crate está no [crates.io](https://crates.io/crates/molt-framework) e a documentação da API no
[docs.rs](https://docs.rs/molt-framework). Ela é publicada como `molt-framework` (`molt` no
crates.io é um projeto sem relação) e importada como `molt`.
```bash
cargo add molt-framework
```
ou, à mão, no `Cargo.toml`:
```toml
[dependencies]
molt-framework = "1.1"
```
Nunca usou Rust? O guia [`new-to-rust.md`](new-to-rust.md) vai da instalação do Rust até uma
página aberta no navegador, e explica como executar o projeto.
```rust
use molt::prelude::*;
```
Features:
| `ssg` | ✓ | `StaticSiteGenerator`: grava páginas e arquivos estáticos em disco |
| `wasm` | | bindings `wasm-bindgen` para JavaScript |
| `parallel` | | geração de várias páginas em paralelo, com `rayon` |
### Servindo a saída
Molt renderiza uma `String`, e esse é todo o contrato de integração com um servidor web: não
existe feature para `axum`, `actix` ou `tokio`, e nenhuma está planejada, para que o núcleo
continue sem dependências obrigatórias. Com o [Axum](https://crates.io/crates/axum), o `Html` já
aceita uma `String`, então um programa completo, a partir de um `cargo new` vazio, fica assim:
```bash
cargo add molt-framework axum
cargo add tokio --features macros,rt-multi-thread
```
```rust,ignore
use axum::{Router, response::Html, routing::get};
use molt::Document;
use molt::prelude::*;
async fn home() -> Html<String> {
let page = Document::new(Some("en"))
.head_children([meta().attr("charset", "UTF-8"), title().text("Molt + Axum")])
.body_children([h1().text("Track every service")]);
Html(page.render())
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(home));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
```
Rode `cargo run` e abra <http://localhost:3000>. `Document` não está no prelude; importe-o de
`molt`, como acima. Molt não depende de `axum` nem de `tokio`, então o seu projeto precisa
adicioná-los.
Se quiser que um tipo seu seja uma resposta, envolva o documento em um newtype na sua crate e
implemente a trait do framework nele. A regra do órfão permite isso, porque o newtype é seu:
```rust,ignore
use axum::response::{Html, IntoResponse, Response};
struct Page(molt::Document);
impl IntoResponse for Page {
fn into_response(self) -> Response {
Html(self.0.render()).into_response()
}
}
```
O mesmo padrão serve para qualquer framework. Uma crate específica de framework (`molt-axum`,
por exemplo) é bem-vinda como projeto separado que dependa desta; veja o
[`ROADMAP.md`](../../ROADMAP.md).
## O que tem dentro
- **93 elementos HTML**, gerados a partir de uma única tabela. Veja
[`docs/tag-catalog.md`](../tag-catalog.md).
- **API de builder fluente**: `set_id`, `add_class`, `add_classes`, `set_style`, `attr`,
`data_attr(s)`, `aria_attr(s)`, `set_role`, `child`, `text`, `raw_text`
- **Macro `html!`**: aninhamento, atributos, `@if` / `@else`, `@for`
- **`Document`**: cuida do doctype e renderiza indentado por padrão
- **Trait `Layout`**: estruturas de página reutilizáveis
- **SEO**: Open Graph, Twitter Cards, o bloco `<meta>` comum e um `SeoBuilder`
- **Acessibilidade**: helpers de ARIA e uma auditoria que encontra imagens sem `alt`, botões
sem nome acessível e iframes sem título
- **Sitemaps e RSS 2.0**
- **Geração de site estático**: gravação atômica, cópia de arquivos estáticos, paralelismo
opcional
- **WebAssembly**: a mesma API a partir de JavaScript
## WebAssembly
```bash
wasm-pack build --target web --features wasm
```
```js
import init, { element, WDocument } from "./pkg/molt.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();
```
Os bindings envolvem a árvore real, em vez de acumular strings já renderizadas. Por isso
`renderPretty()` funciona e o JavaScript usa exatamente o mesmo renderizador que o Rust.
**Os handles são consumidos.** Os métodos do builder e os argumentos de elemento movem o valor
para dentro do Rust. Um handle que você já repassou (ou do qual já encadeou uma chamada) deixa
de existir: usá-lo lança `null pointer passed to rust`. Encadeie as chamadas, ou guarde o
handle que cada chamada devolve.
## Limitações conhecidas
**A saída indentada cresce com o quadrado da profundidade.** Cada linha carrega uma indentação
por nível acima dela, então uma árvore muito profunda gera uma string muito grande. A
renderização em si não tem limite de profundidade (o escritor usa uma pilha explícita em vez de
recursão), mas vale conhecer esse tamanho quando a profundidade vem de uma entrada não
confiável. Veja o [`SECURITY.md`](../../SECURITY.md#rendering-depth-is-no-longer-bounded-by-the-stack)
(em inglês).
## Documentação
- [`new-to-rust.md`](new-to-rust.md): nunca usou Rust? Instale, crie um projeto, adicione o
Molt e execute.
- [`GETTING_STARTED.md`](GETTING_STARTED.md): uma primeira página, escape, `html!`, SEO e um
site em disco.
- [`MIGRATION.md`](../../MIGRATION.md) (inglês): da 0.1.0 para a 1.0.0, mudança por mudança.
- [`SECURITY.md`](../../SECURITY.md) (inglês): o que é garantido, os limites e como relatar um
problema.
- [`ROADMAP.md`](../../ROADMAP.md) e [`CHANGELOG.md`](../../CHANGELOG.md) (inglês).
- Referência da API no [docs.rs](https://docs.rs/molt-framework).
## Desenvolvimento
```bash
./scripts/verify.sh # build, testes, clippy, fmt, catálogo, fixtures, smoke test
cargo test # testes unitários, de integração e de documentação
cargo bench # desempenho da renderização
```
`verify.sh` é um superconjunto do CI. Rode-o antes de dizer que uma mudança está pronta.
## Contribuindo
Molt é código aberto e contribuições são bem-vindas. Um relato de bug com a entrada que o
provocou é tão útil quanto um patch.
- **Encontrou um bug ou quer uma funcionalidade?**
[Abra uma issue](https://github.com/micheltlutz/molt/issues). O
[`ROADMAP.md`](../../ROADMAP.md) diz o que está planejado, o que é só uma pista e o que está
fora de propósito.
- **Encontrou um problema de segurança?** Por favor, não abra uma issue pública; use um
[aviso de segurança privado](https://github.com/micheltlutz/molt/security/advisories/new).
Veja o [`SECURITY.md`](../../SECURITY.md).
- **Vai enviar código?** Crie a branch a partir de `develop`, adicione um teste que verifique a
marcação renderizada e rode `./scripts/verify.sh` antes de abrir o pull request. As regras e
seus motivos estão no [`CONTRIBUTING.md`](../../CONTRIBUTING.md).
- **Usa um agente de IA?** Aponte-o para o [`AGENTS.md`](../../AGENTS.md), o mapa das regras em
[`.claude/rules`](../../.claude/rules), dos slash commands, dos agentes de revisão e da base
de conhecimento em [`.claude/skills/molt`](../../.claude/skills/molt/SKILL.md): regras de
segurança, práticas de Rust e as lições aprendidas até chegar à 1.0. O estilo de código que
todo contribuidor segue está em
[`.claude/rules/code-style.md`](../../.claude/rules/code-style.md).
### Apoie o projeto
Se o Molt economiza o seu tempo, você pode ajudar a mantê-lo
**[patrocinando no GitHub](https://github.com/sponsors/micheltlutz)**. Dar uma estrela no
repositório, divulgá-lo e me contar o que você construiu com ele ajudam tanto quanto.
## O nome
*Molt* (muda) é a troca do exoesqueleto, o jeito como um crustáceo cresce: uma homenagem ao
Ferris e a de onde este projeto veio. Ele começou como um port de uma biblioteca de HTML em
Swift, criada para rodar produtos SaaS comerciais com pouca infraestrutura na nuvem, e foi para
Rust pela eficiência e pelas garantias. A casca que ele manteve é a API; o que ele largou foi o
excesso.
## Licença
MIT. Veja o [`LICENSE`](../../LICENSE).
Criado por [Michel Anderson Lutz Teixeira](https://micheltlutz.me).