├── examples
└── htmx-rsx
│ ├── .gitignore
│ ├── tailwind.css
│ ├── src
│ ├── views
│ │ ├── mod.rs
│ │ ├── home.rs
│ │ ├── about.rs
│ │ ├── list.rs
│ │ ├── document.rs
│ │ └── nav.rs
│ ├── main.rs
│ └── handlers.rs
│ ├── Cargo.toml
│ ├── build.rs
│ └── README.md
├── hypertext-macros
├── src
│ ├── html
│ │ ├── syntaxes
│ │ │ ├── mod.rs
│ │ │ ├── maud.rs
│ │ │ └── rsx.rs
│ │ ├── component.rs
│ │ ├── control.rs
│ │ ├── basics.rs
│ │ └── generate.rs
│ ├── lib.rs
│ ├── component.rs
│ └── derive.rs
└── Cargo.toml
├── rustfmt.toml
├── deny.toml
├── .github
├── dependabot.yaml
└── workflows
│ ├── security-audit.yaml
│ └── ci.yaml
├── hypertext
├── src
│ ├── prelude.rs
│ ├── context.rs
│ ├── macros
│ │ ├── mod.rs
│ │ └── alloc.rs
│ ├── validation
│ │ ├── mathml.rs
│ │ ├── mod.rs
│ │ └── attributes.rs
│ ├── web_frameworks.rs
│ ├── alloc
│ │ ├── impls.rs
│ │ └── mod.rs
│ └── lib.rs
├── Cargo.toml
└── tests
│ └── main.rs
├── Cargo.toml
├── LICENSE.txt
├── release-plz.toml
├── README.md
├── .gitignore
└── CHANGELOG.md
/examples/htmx-rsx/.gitignore:
--------------------------------------------------------------------------------
1 | static/
2 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/tailwind.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
--------------------------------------------------------------------------------
/hypertext-macros/src/html/syntaxes/mod.rs:
--------------------------------------------------------------------------------
1 | mod maud;
2 | mod rsx;
3 |
4 | pub use self::{maud::Maud, rsx::Rsx};
5 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/views/mod.rs:
--------------------------------------------------------------------------------
1 | mod about;
2 | mod document;
3 | mod home;
4 | mod list;
5 | mod nav;
6 |
7 | pub use self::{about::*, document::*, home::*, list::*, nav::*};
8 |
--------------------------------------------------------------------------------
/rustfmt.toml:
--------------------------------------------------------------------------------
1 | unstable_features = true
2 | format_code_in_doc_comments = true
3 | format_macro_matchers = true
4 | group_imports = "StdExternalCrate"
5 | imports_granularity = "Crate"
6 | reorder_impl_items = true
7 | wrap_comments = true
8 |
--------------------------------------------------------------------------------
/deny.toml:
--------------------------------------------------------------------------------
1 | [licenses]
2 | allow = ["Apache-2.0", "BSD-3-Clause", "MIT", "Unicode-3.0", "Zlib"]
3 |
4 | [advisories]
5 | ignore = [
6 | "RUSTSEC-2020-0056",
7 | "RUSTSEC-2021-0059",
8 | "RUSTSEC-2021-0060",
9 | "RUSTSEC-2021-0064",
10 | "RUSTSEC-2024-0384",
11 | ]
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: "cargo"
5 | directory: "/"
6 | schedule:
7 | interval: "daily"
8 |
9 | - package-ecosystem: "github-actions"
10 | directory: "/"
11 | schedule:
12 | interval: "daily"
13 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "htmx-rsx"
3 |
4 | edition.workspace = true
5 | license.workspace = true
6 | publish = false
7 | version = "0.0.0"
8 |
9 | [dependencies]
10 | anyhow = "1"
11 | axum = "0.8"
12 | axum-htmx = "0.8"
13 | hypertext = { path = "../../hypertext", features = ["axum", "htmx"] }
14 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
15 | tower-http = { version = "0.6", features = ["fs"] }
16 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/views/home.rs:
--------------------------------------------------------------------------------
1 | use hypertext::prelude::*;
2 |
3 | #[component]
4 | pub fn home() -> impl Renderable {
5 | rsx! {
6 |
7 |
"Welcome to HTMX-RSX"
8 |
"This is a simple example of using HTMX with RSX."
9 |
"Click the links in the navigation bar to explore."
10 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/views/about.rs:
--------------------------------------------------------------------------------
1 | use hypertext::prelude::*;
2 |
3 | #[component]
4 | pub fn about() -> impl Renderable {
5 | rsx! {
6 |
7 |
"About HTMX-RSX"
8 |
"HTMX-RSX is a simple example of using HTMX with RSX."
9 |
"This project demonstrates how to use HTMX for dynamic content loading in a Rust web application."
10 |
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.github/workflows/security-audit.yaml:
--------------------------------------------------------------------------------
1 | name: Security Audit
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 |
7 | push:
8 | paths:
9 | - "**/Cargo.toml"
10 | - "**/Cargo.lock"
11 | - "**/deny.toml"
12 |
13 | pull_request:
14 |
15 | jobs:
16 | cargo-deny:
17 | name: Security Audit
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - name: Checkout repository
23 | uses: actions/checkout@v4
24 |
25 | - name: Run `cargo-deny`
26 | uses: EmbarkStudios/cargo-deny-action@v2
27 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/views/list.rs:
--------------------------------------------------------------------------------
1 | use hypertext::prelude::*;
2 |
3 | #[component]
4 | pub fn list() -> impl Renderable {
5 | let list_items = vec!["Hypertext", "is", "fun!"];
6 | rsx! {
7 |
8 |
"Loop through items using Rust code!"
9 |
10 | @for item in &list_items {
11 | (item)
12 | }
13 |
14 |
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/hypertext/src/prelude.rs:
--------------------------------------------------------------------------------
1 | //! Re-exported items for convenience.
2 | //!
3 | //! This module re-exports all the commonly used items from the crate,
4 | //! so you can use them without having to import them individually. It also
5 | //! re-exports the [`hypertext_elements`] module, and any [framework-specific
6 | //! attribute traits](crate::validation::attributes) that have been enabled, as
7 | //! well as the [`GlobalAttributes`] trait.
8 | pub use crate::validation::{attributes::*, hypertext_elements};
9 | #[cfg(feature = "alloc")]
10 | pub use crate::{Renderable, RenderableExt as _, Rendered, attribute, component, maud, rsx};
11 |
--------------------------------------------------------------------------------
/hypertext-macros/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "hypertext-macros"
3 | version.workspace = true
4 | authors.workspace = true
5 | edition.workspace = true
6 | description.workspace = true
7 | documentation = "https://docs.rs/hypertext-macros"
8 | readme.workspace = true
9 | homepage.workspace = true
10 | repository.workspace = true
11 | license.workspace = true
12 | keywords.workspace = true
13 | categories.workspace = true
14 |
15 | [lib]
16 | proc-macro = true
17 |
18 | [dependencies]
19 | html-escape.workspace = true
20 | proc-macro2 = "1"
21 | quote = "1"
22 | syn = { version = "2", features = ["extra-traits", "full"] }
23 |
24 | [lints]
25 | workspace = true
26 |
27 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/build.rs:
--------------------------------------------------------------------------------
1 | use std::{error::Error, process::Command};
2 |
3 | const TAILWIND_CSS: &str = "tailwind.css";
4 |
5 | fn main() -> Result<(), Box> {
6 | println!("cargo:rerun-if-changed={TAILWIND_CSS}");
7 | println!("cargo:rerun-if-changed=src/views/");
8 |
9 | let output = Command::new("tailwindcss")
10 | .args(["-i", TAILWIND_CSS, "-o", "static/styles.css", "--minify"])
11 | .output()?;
12 |
13 | if !output.status.success() {
14 | return Err(format!(
15 | "failed to execute `tailwindcss`:\n{}",
16 | String::from_utf8_lossy(&output.stderr)
17 | )
18 | .into());
19 | }
20 |
21 | Ok(())
22 | }
23 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::net::Ipv4Addr;
2 |
3 | use anyhow::Result;
4 | use axum::{Router, routing::get};
5 | use handlers::{handle_about, handle_home, handle_list};
6 | use tokio::net::TcpListener;
7 | use tower_http::services::ServeDir;
8 |
9 | mod handlers;
10 | mod views;
11 |
12 | #[tokio::main]
13 | async fn main() -> Result<()> {
14 | // build our application with a route
15 | let app = Router::new()
16 | .route("/", get(handle_home))
17 | .route("/about", get(handle_about))
18 | .route("/list", get(handle_list))
19 | .fallback_service(ServeDir::new("static"));
20 |
21 | // run our app with hyper, listening globally on port 3000
22 | let listener = TcpListener::bind((Ipv4Addr::UNSPECIFIED, 3000)).await?;
23 |
24 | axum::serve(listener, app).await?;
25 |
26 | Ok(())
27 | }
28 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/handlers.rs:
--------------------------------------------------------------------------------
1 | use axum::response::IntoResponse;
2 | use axum_htmx::HxRequest;
3 | use hypertext::prelude::*;
4 |
5 | use crate::views::{Document, Nav, about, home, list};
6 |
7 | fn maybe_document(
8 | HxRequest(is_hx_request): HxRequest,
9 | selected: &str,
10 | children: R,
11 | ) -> impl IntoResponse {
12 | rsx! {
13 | @if is_hx_request {
14 |
15 | (children)
16 | } @else {
17 |
18 | (children)
19 |
20 | }
21 | }
22 | }
23 |
24 | pub async fn handle_home(hx_request: HxRequest) -> impl IntoResponse {
25 | maybe_document(hx_request, "/", home())
26 | }
27 |
28 | pub async fn handle_about(hx_request: HxRequest) -> impl IntoResponse {
29 | maybe_document(hx_request, "/about", about())
30 | }
31 |
32 | pub async fn handle_list(hx_request: HxRequest) -> impl IntoResponse {
33 | maybe_document(hx_request, "/list", list())
34 | }
35 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/views/document.rs:
--------------------------------------------------------------------------------
1 | use hypertext::prelude::*;
2 |
3 | use crate::views::nav::Nav;
4 |
5 | #[component]
6 | pub fn document<'a, R: Renderable>(selected: &'a str, children: &R) -> impl Renderable {
7 | rsx! {
8 |
9 |
10 |
11 | "Hypertext - HTMX with RSX"
12 |
13 |
17 |
18 |
19 |
20 |
21 | Hypertext
22 |
23 |
24 | (children)
25 |
26 |
27 |
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | resolver = "2"
3 | members = ["examples/*", "hypertext", "hypertext-macros"]
4 | default-members = ["hypertext", "hypertext-macros"]
5 |
6 | [workspace.package]
7 | version = "0.12.1"
8 | authors = ["Vidhan Bhatt "]
9 | edition = "2024"
10 | description = "A blazing fast type-checked HTML macro crate."
11 | readme = "README.md"
12 | homepage = "https://github.com/vidhanio/hypertext"
13 | repository = "https://github.com/vidhanio/hypertext"
14 | license = "MIT"
15 | keywords = ["html", "macro"]
16 | categories = ["template-engine"]
17 |
18 | [workspace.dependencies]
19 | html-escape = { version = "0.2", default-features = false }
20 | hypertext-macros = { version = "0.12.1", path = "./hypertext-macros" }
21 |
22 | [workspace.lints]
23 | [workspace.lints.clippy]
24 | cargo = { level = "warn", priority = -1 }
25 | nursery = { level = "warn", priority = -1 }
26 | pedantic = { level = "warn", priority = -1 }
27 | too_long_first_doc_paragraph = "allow"
28 |
29 | [workspace.lints.rust]
30 | missing_copy_implementations = "warn"
31 | missing_debug_implementations = "warn"
32 | missing_docs = "warn"
33 |
34 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/src/views/nav.rs:
--------------------------------------------------------------------------------
1 | use hypertext::prelude::*;
2 |
3 | #[component]
4 | pub fn nav<'a>(selected: &'a str, oob: bool) -> impl Renderable {
5 | let routes = [("Home", "/"), ("About", "/about"), ("List", "/list")];
6 |
7 | rsx! {
8 |
9 |
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/release-plz.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | git_release_enable = false
3 | dependencies_update = true
4 | changelog_update = false
5 |
6 | [[package]]
7 | name = "hypertext"
8 | changelog_update = true
9 | changelog_include = [
10 | "hypertext-macros",
11 | ]
12 | changelog_path = "./CHANGELOG.md"
13 | git_release_enable = true
14 |
15 | [changelog]
16 | body = """
17 |
18 | ## [{{ version }}]{% if release_link %}({{ release_link }}){% endif %} - {{ timestamp | date(format="%Y-%m-%d") }}
19 | {% for group, commits in commits | group_by(attribute="group") %}
20 | ### {{ group | striptags }}
21 | {% for commit in commits %}
22 | - {% if commit.scope %}*({{commit.scope}})* {% endif %}\
23 | {% if commit.breaking %}[**breaking**] {% endif %}\
24 | {{ commit.message }}\
25 | {% if commit.remote.pr_number %} (#{{ commit.remote.pr_number }}){% endif -%}
26 | {% endfor %}
27 | {% endfor %}
28 | """
29 | commit_parsers = [
30 | { message = "(?i)^.*(change|switch|swap).*$", group = "Changed" },
31 | { message = "(?i)^.*deprecate.*$", group = "Deprecated" },
32 | { message = "(?i)^.*(remove|delete).*$", group = "Removed" },
33 | { message = "(?i)^.*security.*$", group = "Security" },
34 | { message = '^feat(\(.*?\))?!?:.*$', group = "Added" },
35 | { message = '^fix(\(.*?\))?!?:.*$', group = "Fixed" },
36 | { message = ".*", group = "Other" },
37 | ]
--------------------------------------------------------------------------------
/hypertext/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "hypertext"
3 | version.workspace = true
4 | authors.workspace = true
5 | edition.workspace = true
6 | description.workspace = true
7 | documentation = "https://docs.rs/hypertext"
8 | readme.workspace = true
9 | homepage.workspace = true
10 | repository.workspace = true
11 | license.workspace = true
12 | keywords.workspace = true
13 | categories.workspace = true
14 |
15 | [package.metadata.docs.rs]
16 | all-features = true
17 |
18 | [dependencies]
19 | actix-web = { version = "4", default-features = false, optional = true }
20 | axum-core = { version = "0.5", default-features = false, optional = true }
21 | html-escape = { workspace = true, optional = true }
22 | hypertext-macros.workspace = true
23 | itoa = { version = "1", optional = true }
24 | ntex = { version = "2", default-features = false, optional = true }
25 | poem = { version = "3", default-features = false, optional = true }
26 | rocket = { version = "0.5", default-features = false, optional = true }
27 | ryu = { version = "1", optional = true }
28 | salvo_core = { version = "0.81", default-features = false, optional = true }
29 | tide = { version = "0.16", default-features = false, optional = true }
30 | warp = { version = "0.4", default-features = false, optional = true }
31 |
32 | [features]
33 | actix-web = ["alloc", "dep:actix-web"]
34 | alloc = ["dep:html-escape", "dep:itoa", "dep:ryu"]
35 | alpine = []
36 | axum = ["alloc", "dep:axum-core"]
37 | default = ["alloc"]
38 | htmx = []
39 | hyperscript = []
40 | mathml = []
41 | ntex = ["alloc", "dep:ntex"]
42 | poem = ["alloc", "dep:poem"]
43 | rocket = ["alloc", "dep:rocket"]
44 | salvo = ["alloc", "dep:salvo_core"]
45 | tide = ["alloc", "dep:tide"]
46 | warp = ["alloc", "dep:warp"]
47 |
48 | [lints]
49 | workspace = true
50 |
51 |
--------------------------------------------------------------------------------
/examples/htmx-rsx/README.md:
--------------------------------------------------------------------------------
1 | # Hypertext HTMX RSX Example
2 |
3 | ## Setup
4 |
5 | First, install `npm` packages (Tailwind CSS CLI)
6 |
7 | ```sh
8 | npm i
9 | ```
10 |
11 | Next, install [air](https://github.com/air-verse/air) for automatic reload (make sure you have [Go installed](https://go.dev/doc/install)):
12 |
13 | ```sh
14 | go install github.com/air-verse/air@latest
15 | ```
16 |
17 | Start the server:
18 |
19 | ```sh
20 | air
21 | ```
22 |
23 | Open `localhost:3001` in your browser!
24 |
25 | ## Design
26 |
27 | The `views` folder contains any HTML templates.
28 | The `handlers` folder contains any `axum` handlers used for routing.
29 |
30 | ### Components
31 |
32 | With `hypertext` you can use Rust functions as re-usable HTML components! Simply set the return type to `impl Renderable` and you can
33 | reference that function to call your component.
34 |
35 | ```rust
36 | use hypertext::prelude::*;
37 |
38 | use crate::views::nav;
39 |
40 | pub fn about(nav_oob: bool) -> impl Renderable {
41 | rsx! {
42 | @if nav_oob {
43 | { nav("/", true) }
44 | }
45 |
46 |
"About HTMX-RSX"
47 |
"HTMX-RSX is a simple example of using HTMX with RSX."
48 |
"This project demonstrates how to use HTMX for dynamic content loading in a Rust web application."
49 |
50 | }
51 | }
52 | ```
53 |
54 | You can even pass a component into another component as a parameter!
55 |
56 | In this example we are setting a parameter `page` so that any component can be passed into this one.
57 |
58 | ```rust
59 | pub fn index(selected: &str, page: impl Renderable) -> impl Renderable {
60 | // ...
61 | }
62 | ```
63 |
--------------------------------------------------------------------------------
/hypertext/src/context.rs:
--------------------------------------------------------------------------------
1 | //! The [`Context`] trait and its implementors.
2 |
3 | /// A marker trait to represent the context that the value is being rendered to.
4 | ///
5 | /// This can be either [`Node`] or an [`AttributeValue`]. A [`Node`]
6 | /// represents an HTML node, while an [`AttributeValue`] represents an attribute
7 | /// value which will eventually be surrounded by double quotes.
8 | ///
9 | /// This is used to ensure that the correct rendering methods are called
10 | /// for each context, and to prevent errors such as accidentally rendering
11 | /// an HTML element into an attribute value.
12 | pub trait Context: sealed::Sealed {}
13 |
14 | /// A marker type to represent a complete element node.
15 | ///
16 | /// All types and traits that are generic over [`Context`] use [`Node`]
17 | /// as the default for the generic type parameter.
18 | ///
19 | /// Traits and types with this marker type expect complete HTML nodes. If
20 | /// rendering string-like types, the value/implementation must escape `&` to
21 | /// `&`, `<` to `<`, and `>` to `>`.
22 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
23 | pub struct Node;
24 |
25 | impl Context for Node {}
26 |
27 | /// A marker type to represent an attribute value.
28 | ///
29 | /// Traits and types with this marker type expect an attribute value which will
30 | /// eventually be surrounded by double quotes. The value/implementation must
31 | /// escape `&` to `&`, `<` to `<`, `>` to `>`, and `"` to `"`.
32 | #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
33 | pub struct AttributeValue;
34 |
35 | impl Context for AttributeValue {}
36 |
37 | mod sealed {
38 | use super::{AttributeValue, Node};
39 |
40 | pub trait Sealed {}
41 | impl Sealed for Node {}
42 | impl Sealed for AttributeValue {}
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # `hypertext`
2 |
3 | A blazing fast type-checked HTML macro crate.
4 |
5 | ## Features
6 |
7 | - Type checking for element names/attributes, including extensible support for custom frameworks like [htmx](https://htmx.org/) and [Alpine.js](https://alpinejs.dev/)
8 | - `#![no_std]` support
9 | - [Extremely fast](https://github.com/askama-rs/template-benchmark#benchmark-results),
10 | using lazy rendering to minimize allocation
11 | - Integration with all major web frameworks
12 |
13 | ## Example
14 |
15 | ```rust
16 | use hypertext::prelude::*;
17 |
18 | let shopping_list = ["milk", "eggs", "bread"];
19 |
20 | let shopping_list_maud = maud! {
21 | div {
22 | h1 { "Shopping List" }
23 | ul {
24 | @for (i, item) in (1..).zip(shopping_list) {
25 | li.item {
26 | input #{ "item-" (i) } type="checkbox";
27 | label for={ "item-" (i) } { (item) }
28 | }
29 | }
30 | }
31 | }
32 | }
33 | .render();
34 |
35 | // or, alternatively:
36 |
37 | let shopping_list_rsx = rsx! {
38 |
39 |
Shopping List
40 |
48 |
49 | }
50 | .render();
51 | ```
52 |
53 | ## Projects Using `hypertext`
54 |
55 | - [vidhan.io](https://github.com/vidhanio/site) (my website!)
56 | - [The Brainmade Mark](https://github.com/0atman/BrainMade-org)
57 | - [Lipstick on a pig -- a website for hosting volunteer-built tarballs for KISS Linux](https://github.com/kiedtl/loap)
58 | - [web.youwen.dev](https://web.youwen.dev) ― [@youwen5](https://github.com/youwen5)'s personal website
59 |
60 | Make a pull request to list your project here!
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig
2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,rust
3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,rust
4 |
5 | ### macOS ###
6 | # General
7 | .DS_Store
8 | .AppleDouble
9 | .LSOverride
10 |
11 | # Icon must end with two \r
12 | Icon
13 |
14 | # Thumbnails
15 | ._*
16 |
17 | # Files that might appear in the root of a volume
18 | .DocumentRevisions-V100
19 | .fseventsd
20 | .Spotlight-V100
21 | .TemporaryItems
22 | .Trashes
23 | .VolumeIcon.icns
24 | .com.apple.timemachine.donotpresent
25 |
26 | # Directories potentially created on remote AFP share
27 | .AppleDB
28 | .AppleDesktop
29 | Network Trash Folder
30 | Temporary Items
31 | .apdisk
32 |
33 | ### macOS Patch ###
34 | # iCloud generated files
35 | *.icloud
36 |
37 | ### Rust ###
38 | # Generated by Cargo
39 | # will have compiled files and executables
40 | debug/
41 | target/
42 |
43 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
44 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
45 | # Cargo.lock
46 |
47 | # These are backup files generated by rustfmt
48 | **/*.rs.bk
49 |
50 | # MSVC Windows builds of rustc generate these, which store debugging information
51 | *.pdb
52 |
53 | ### VisualStudioCode ###
54 | .vscode/*
55 | !.vscode/settings.json
56 | !.vscode/tasks.json
57 | !.vscode/launch.json
58 | !.vscode/extensions.json
59 | !.vscode/*.code-snippets
60 |
61 | # Local History for Visual Studio Code
62 | .history/
63 |
64 | # Built Visual Studio Code Extensions
65 | *.vsix
66 |
67 | ### VisualStudioCode Patch ###
68 | # Ignore all local history of files
69 | .history
70 | .ionide
71 |
72 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,rust
73 |
74 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
75 | tmp/
76 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ## [0.12.1](https://github.com/vidhanio/hypertext/compare/hypertext-v0.12.0...hypertext-v0.12.1) - 2025-08-09
11 |
12 | ### Other
13 |
14 | - cleanup docs and implementations
15 | - make len estimate better
16 |
17 | ## [0.12.0](https://github.com/vidhanio/hypertext/compare/hypertext-v0.11.0...hypertext-v0.12.0) - 2025-08-09
18 |
19 | ### Added
20 |
21 | - [**breaking**] unify all duplicated `Attribute` versions of types/traits with the `Context` trait
22 | - [**breaking**] make it harder to accidentally make your code vulnerable to XSS via `Buffer`, hiding
23 | constructors, and `dangerously_*` functions
24 |
25 | ### Changed
26 |
27 | - [**breaking**] rename `html_elements` to `hypertext_elements`
28 | - [**breaking**] rename `[void_]elements!` to `define_[void_]elements!`
29 | - [**breaking**] reorganize all validation-related modules into `validation`
30 |
31 | ## [0.11.0](https://github.com/vidhanio/hypertext/compare/hypertext-v0.10.0...hypertext-v0.11.0) - 2025-08-06
32 |
33 | ### Added
34 |
35 | - [**breaking**] add handler/`role` attributes and re-org attribute traits (fixes #136)
36 | - export `void_elements!` (fixes #132)
37 | - [**breaking**] support `:` in maud class syntax (fixes #129)
38 | - add custom vis support to `#[component]`
39 | - add ntex support
40 | - make struct unit if no args
41 | - reduce syn feature tree
42 |
43 | ### Fixed
44 |
45 | - only run tests in alloc
46 | - suppress errors about unused delims on toggles (fixes #130)
47 |
48 | ### Other
49 |
50 | - Bump warp from 0.3.7 to 0.4.0 ([#137](https://github.com/vidhanio/hypertext/pull/137))
51 | - get rid of extra `http` dep
52 | - simplify features and macros
53 | - fix docs and ci
54 | - correct `*_static!` mention
55 | - clean up lint rules
56 | - add mathml/web components info
57 | - re-add syn features
58 |
59 | ## [0.10.0](https://github.com/vidhanio/hypertext/compare/hypertext-v0.9.0...hypertext-v0.10.0) - 2025-07-28
60 |
61 | ### Fixed
62 |
63 | - [**breaking**] add check for quotes
64 |
--------------------------------------------------------------------------------
/hypertext/src/macros/mod.rs:
--------------------------------------------------------------------------------
1 | #[cfg(feature = "alloc")]
2 | mod alloc;
3 |
4 | /// Generate static HTML attributes.
5 | ///
6 | /// This will return a [`RawAttribute<&'static str>`](crate::RawAttribute),
7 | /// which can be used in `const` contexts.
8 | ///
9 | /// Note that the macro cannot process any dynamic content, so you cannot use
10 | /// any expressions inside the macro.
11 | ///
12 | /// # Example
13 | ///
14 | /// ```
15 | /// use hypertext::{RawAttribute, attribute_static, prelude::*};
16 | ///
17 | /// assert_eq!(
18 | /// attribute_static! { "my attribute " 1 }.into_inner(),
19 | /// "my attribute 1"
20 | /// );
21 | /// ```
22 | pub use hypertext_macros::attribute_static;
23 | /// Generate static HTML using [`maud`] syntax.
24 | ///
25 | /// For details about the syntax, see [`maud!`].
26 | ///
27 | /// This will return a [`Raw<&'static str>`](crate::Raw), which can be used in
28 | /// `const` contexts.
29 | ///
30 | /// Note that the macro cannot process any dynamic content, so you cannot use
31 | /// any expressions inside the macro.
32 | ///
33 | /// # Example
34 | ///
35 | /// ```
36 | /// use hypertext::{Raw, maud_static, prelude::*};
37 | ///
38 | /// assert_eq!(
39 | /// maud_static! {
40 | /// div #profile title="Profile" {
41 | /// h1 { "Alice" }
42 | /// }
43 | /// }
44 | /// .into_inner(),
45 | /// r#"
Alice "#,
46 | /// );
47 | /// ```
48 | ///
49 | /// [`maud`]: https://docs.rs/maud
50 | pub use hypertext_macros::maud_static;
51 | /// Generate static HTML using rsx syntax.
52 | ///
53 | /// This will return a [`Raw<&'static str>`](crate::Raw), which can be used in
54 | /// `const` contexts.
55 | ///
56 | /// Note that the macro cannot process any dynamic content, so you cannot use
57 | /// any expressions inside the macro.
58 | ///
59 | /// # Example
60 | ///
61 | /// ```
62 | /// use hypertext::{Raw, prelude::*, rsx_static};
63 | ///
64 | /// assert_eq!(
65 | /// rsx_static! {
66 | ///
67 | ///
Alice
68 | ///
69 | /// }
70 | /// .into_inner(),
71 | /// r#"
Alice "#,
72 | /// );
73 | /// ```
74 | pub use hypertext_macros::rsx_static;
75 |
76 | #[cfg(feature = "alloc")]
77 | pub use self::alloc::*;
78 |
--------------------------------------------------------------------------------
/hypertext-macros/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![expect(missing_docs)]
2 | #![cfg_attr(docsrs, feature(doc_auto_cfg))]
3 |
4 | mod component;
5 | mod derive;
6 | mod html;
7 |
8 | use html::{AttributeValueNode, Nodes};
9 | use proc_macro::TokenStream;
10 | use syn::{DeriveInput, ItemFn, parse::Parse, parse_macro_input};
11 |
12 | use self::html::{Document, Maud, Rsx, Syntax};
13 | use crate::{component::ComponentArgs, html::generate::Context};
14 |
15 | #[proc_macro]
16 | pub fn maud(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
17 | lazy::(tokens, true)
18 | }
19 |
20 | #[proc_macro]
21 | pub fn maud_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
22 | lazy::(tokens, false)
23 | }
24 |
25 | #[proc_macro]
26 | pub fn rsx(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
27 | lazy::(tokens, true)
28 | }
29 |
30 | #[proc_macro]
31 | pub fn rsx_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
32 | lazy::(tokens, false)
33 | }
34 |
35 | #[proc_macro]
36 | pub fn maud_static(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
37 | static_::(tokens)
38 | }
39 |
40 | #[proc_macro]
41 | pub fn rsx_static(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
42 | static_::(tokens)
43 | }
44 |
45 | fn lazy(tokens: proc_macro::TokenStream, move_: bool) -> proc_macro::TokenStream
46 | where
47 | Document: Parse,
48 | {
49 | html::generate::lazy::>(tokens.into(), move_)
50 | .unwrap_or_else(|err| err.to_compile_error())
51 | .into()
52 | }
53 |
54 | fn static_(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream
55 | where
56 | Document: Parse,
57 | {
58 | html::generate::literal::>(tokens.into())
59 | .unwrap_or_else(|err| err.to_compile_error())
60 | .into()
61 | }
62 |
63 | #[proc_macro]
64 | pub fn attribute(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
65 | attribute_lazy(tokens, true)
66 | }
67 |
68 | #[proc_macro]
69 | pub fn attribute_borrow(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
70 | attribute_lazy(tokens, false)
71 | }
72 |
73 | fn attribute_lazy(tokens: proc_macro::TokenStream, move_: bool) -> proc_macro::TokenStream {
74 | html::generate::lazy::>(tokens.into(), move_)
75 | .unwrap_or_else(|err| err.to_compile_error())
76 | .into()
77 | }
78 |
79 | #[proc_macro]
80 | pub fn attribute_static(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
81 | html::generate::literal::>(tokens.into())
82 | .unwrap_or_else(|err| err.to_compile_error())
83 | .into()
84 | }
85 |
86 | #[proc_macro_derive(Renderable, attributes(maud, rsx, attribute))]
87 | pub fn derive_renderable(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
88 | derive::renderable(parse_macro_input!(input as DeriveInput))
89 | .unwrap_or_else(|err| err.to_compile_error())
90 | .into()
91 | }
92 |
93 | #[proc_macro_attribute]
94 | pub fn component(attr: TokenStream, item: TokenStream) -> TokenStream {
95 | let attr = parse_macro_input!(attr as ComponentArgs);
96 | let item = parse_macro_input!(item as ItemFn);
97 |
98 | component::generate(attr, &item)
99 | .unwrap_or_else(|err| err.to_compile_error())
100 | .into()
101 | }
102 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | - push
5 | - pull_request
6 |
7 | env:
8 | RUSTFLAGS: -D warnings
9 | RUSTDOCFLAGS: -D warnings
10 |
11 | permissions:
12 | contents: read
13 |
14 | jobs:
15 | test:
16 | name: Test
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - name: Checkout repository
22 | uses: actions/checkout@v4
23 |
24 | - name: Install Rust
25 | uses: dtolnay/rust-toolchain@stable
26 |
27 | - name: Cache dependencies
28 | uses: Swatinem/rust-cache@v2
29 |
30 | - name: Run tests
31 | run: cargo test --all-features
32 |
33 | miri:
34 | name: Miri
35 |
36 | runs-on: ubuntu-latest
37 |
38 | steps:
39 | - name: Checkout repository
40 | uses: actions/checkout@v4
41 |
42 | - name: Install Rust
43 | uses: dtolnay/rust-toolchain@nightly
44 | with:
45 | components: miri
46 |
47 | - name: Cache dependencies
48 | uses: Swatinem/rust-cache@v2
49 |
50 | - name: Setup Miri
51 | run: cargo miri setup
52 |
53 | - name: Run Miri
54 | run: cargo miri test --all-features
55 |
56 | check:
57 | name: Check
58 |
59 | runs-on: ubuntu-latest
60 |
61 | steps:
62 | - name: Checkout repository
63 | uses: actions/checkout@v4
64 |
65 | - name: Install Rust
66 | uses: dtolnay/rust-toolchain@stable
67 | with:
68 | components: clippy
69 |
70 | - name: Cache dependencies
71 | uses: Swatinem/rust-cache@v2
72 |
73 | - name: Check code
74 | run: cargo clippy --all-features
75 |
76 | format:
77 | name: Format
78 |
79 | runs-on: ubuntu-latest
80 |
81 | steps:
82 | - name: Checkout repository
83 | uses: actions/checkout@v4
84 |
85 | - name: Install Rust
86 | uses: dtolnay/rust-toolchain@nightly
87 | with:
88 | components: rustfmt
89 |
90 | - name: Cache dependencies
91 | uses: Swatinem/rust-cache@v2
92 |
93 | - name: Check formatting
94 | run: cargo fmt --all --check
95 |
96 | release:
97 | name: Release
98 |
99 | runs-on: ubuntu-latest
100 |
101 | permissions:
102 | contents: write
103 |
104 | needs:
105 | - test
106 | - miri
107 | - check
108 | - format
109 |
110 | if: github.event_name == 'push' && github.ref == 'refs/heads/main'
111 |
112 | steps:
113 | - name: Checkout repository
114 | uses: actions/checkout@v4
115 | with:
116 | fetch-depth: 0
117 |
118 | - name: Install Rust
119 | uses: dtolnay/rust-toolchain@stable
120 |
121 | - name: Run release-plz
122 | uses: release-plz/action@v0.5
123 | with:
124 | command: release
125 | env:
126 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
127 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
128 |
129 | release-pr:
130 | name: Release PR
131 |
132 | runs-on: ubuntu-latest
133 |
134 | permissions:
135 | contents: write
136 | pull-requests: write
137 |
138 | needs:
139 | - test
140 | - miri
141 | - check
142 | - format
143 |
144 | if: github.event_name == 'push' && github.ref == 'refs/heads/main'
145 |
146 | concurrency:
147 | group: release-${{ github.ref }}
148 | cancel-in-progress: false
149 |
150 | steps:
151 | - name: Checkout repository
152 | uses: actions/checkout@v4
153 | with:
154 | fetch-depth: 0
155 |
156 | - name: Install Rust
157 | uses: dtolnay/rust-toolchain@stable
158 |
159 | - name: Run release-plz
160 | uses: release-plz/action@v0.5
161 | with:
162 | command: release-pr
163 | env:
164 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
165 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
166 |
--------------------------------------------------------------------------------
/hypertext-macros/src/html/component.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::TokenStream;
2 | use quote::{ToTokens, quote, quote_spanned};
3 | use syn::{
4 | Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token,
5 | parse::{Parse, ParseStream},
6 | spanned::Spanned,
7 | token::{Brace, Paren},
8 | };
9 |
10 | use super::{ElementBody, Generate, Generator, Literal, ParenExpr, Syntax};
11 | use crate::{AttributeValueNode, Context};
12 |
13 | pub struct Component {
14 | pub name: Ident,
15 | pub attrs: Vec,
16 | pub dotdot: Option,
17 | pub body: ElementBody,
18 | }
19 |
20 | impl Generate for Component {
21 | const CONTEXT: Context = Context::Node;
22 |
23 | fn generate(&self, g: &mut Generator) {
24 | let fields = self.attrs.iter().map(|attr| {
25 | let name = &attr.name;
26 | let value = &attr.value_expr();
27 |
28 | quote!(#name: #value,)
29 | });
30 |
31 | let children = match &self.body {
32 | ElementBody::Normal { children, .. } => {
33 | let buffer_ident = Generator::buffer_ident();
34 |
35 | let block = g.block_with(Brace::default(), |g| {
36 | g.push(children);
37 | });
38 |
39 | let lazy = quote! {
40 | ::hypertext::Lazy::dangerously_create(
41 | |#buffer_ident: &mut ::hypertext::Buffer|
42 | #block
43 | )
44 | };
45 |
46 | let children_ident = Ident::new("children", self.name.span());
47 |
48 | quote!(
49 | #children_ident: #lazy,
50 | )
51 | }
52 | ElementBody::Void => quote!(),
53 | };
54 |
55 | let name = &self.name;
56 |
57 | let default = self
58 | .dotdot
59 | .as_ref()
60 | .map(|dotdot| quote_spanned!(dotdot.span()=> ..::core::default::Default::default()))
61 | .unwrap_or_default();
62 |
63 | let init = quote! {
64 | #name {
65 | #(#fields)*
66 | #children
67 | #default
68 | }
69 | };
70 |
71 | g.push_expr(Paren::default(), Self::CONTEXT, &init);
72 | }
73 | }
74 |
75 | pub struct ComponentAttribute {
76 | name: Ident,
77 | value: ComponentAttributeValue,
78 | }
79 |
80 | impl ComponentAttribute {
81 | fn value_expr(&self) -> TokenStream {
82 | match &self.value {
83 | ComponentAttributeValue::Literal(lit) => lit.to_token_stream(),
84 | ComponentAttributeValue::Ident(ident) => ident.to_token_stream(),
85 | ComponentAttributeValue::Expr(expr) => {
86 | let mut tokens = TokenStream::new();
87 |
88 | expr.paren_token.surround(&mut tokens, |tokens| {
89 | expr.expr.to_tokens(tokens);
90 | });
91 |
92 | tokens
93 | }
94 | }
95 | }
96 | }
97 |
98 | impl Parse for ComponentAttribute {
99 | fn parse(input: ParseStream) -> syn::Result {
100 | Ok(Self {
101 | name: input.parse()?,
102 | value: {
103 | input.parse::()?;
104 |
105 | input.parse()?
106 | },
107 | })
108 | }
109 | }
110 |
111 | pub enum ComponentAttributeValue {
112 | Literal(Literal),
113 | Ident(Ident),
114 | Expr(ParenExpr),
115 | }
116 |
117 | impl Parse for ComponentAttributeValue {
118 | fn parse(input: ParseStream) -> syn::Result {
119 | let lookahead = input.lookahead1();
120 |
121 | if lookahead.peek(LitStr)
122 | || lookahead.peek(LitInt)
123 | || lookahead.peek(LitBool)
124 | || lookahead.peek(LitFloat)
125 | || lookahead.peek(LitChar)
126 | {
127 | input.call(Literal::parse_any).map(Self::Literal)
128 | } else if lookahead.peek(Ident) {
129 | input.parse().map(Self::Ident)
130 | } else if lookahead.peek(Paren) {
131 | input.parse().map(Self::Expr)
132 | } else {
133 | Err(lookahead.error())
134 | }
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/hypertext-macros/src/component.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::TokenStream;
2 | use quote::quote;
3 | use syn::{FnArg, Ident, ItemFn, Pat, PatType, Type, Visibility, parse::Parse};
4 |
5 | use crate::html::generate::Generator;
6 |
7 | pub struct ComponentArgs {
8 | visibility: Visibility,
9 | ident: Option,
10 | }
11 |
12 | impl Parse for ComponentArgs {
13 | fn parse(input: syn::parse::ParseStream) -> syn::Result {
14 | Ok(Self {
15 | visibility: input.parse()?,
16 | ident: if input.peek(Ident) {
17 | Some(input.parse()?)
18 | } else {
19 | None
20 | },
21 | })
22 | }
23 | }
24 |
25 | pub fn generate(args: ComponentArgs, fn_item: &ItemFn) -> syn::Result {
26 | let mut fields = Vec::new();
27 | let mut field_names = Vec::new();
28 | let mut field_refs = Vec::new();
29 |
30 | let vis = if args.visibility == Visibility::Inherited {
31 | fn_item.vis.clone()
32 | } else {
33 | args.visibility
34 | };
35 |
36 | for input in &fn_item.sig.inputs {
37 | if let FnArg::Typed(PatType { pat, ty, .. }) = input {
38 | let ident = match &**pat {
39 | Pat::Ident(pat_ident) => &pat_ident.ident,
40 | _ => {
41 | return Err(syn::Error::new_spanned(
42 | pat,
43 | "component function parameters must be identifiers",
44 | ));
45 | }
46 | };
47 | let (ty, ref_token) = match &**ty {
48 | Type::Reference(ty_ref) => {
49 | if ty_ref.mutability.is_some() {
50 | return Err(syn::Error::new_spanned(
51 | ty_ref,
52 | "component function parameters cannot be mutable references",
53 | ));
54 | }
55 |
56 | if ty_ref.lifetime.is_some() {
57 | (ty, None)
58 | } else {
59 | (&ty_ref.elem, Some(ty_ref.and_token))
60 | }
61 | }
62 | _ => (ty, None),
63 | };
64 | fields.push(quote! {
65 | #vis #ident: #ty
66 | });
67 | field_names.push(ident.clone());
68 | field_refs.push(ref_token);
69 | } else {
70 | return Err(syn::Error::new_spanned(
71 | input,
72 | "component function parameters do not support `self` or `&self`",
73 | ));
74 | }
75 | }
76 |
77 | let fn_name = &fn_item.sig.ident;
78 |
79 | let struct_name = args
80 | .ident
81 | .unwrap_or_else(|| Ident::new(&to_pascal_case(&fn_name.to_string()), fn_name.span()));
82 |
83 | let (impl_generics, ty_generics, where_clause) = fn_item.sig.generics.split_for_impl();
84 |
85 | let struct_body = if fields.is_empty() {
86 | quote!(;)
87 | } else {
88 | quote! {
89 | { #(#fields),* }
90 | }
91 | };
92 |
93 | let buffer_ident = Generator::buffer_ident();
94 |
95 | let output = quote! {
96 | #[allow(clippy::needless_lifetimes)]
97 | #fn_item
98 |
99 | #vis struct #struct_name #ty_generics #struct_body
100 |
101 | #[automatically_derived]
102 | impl #impl_generics ::hypertext::Renderable for #struct_name #ty_generics #where_clause {
103 | fn render_to(&self, #buffer_ident: &mut ::hypertext::Buffer) {
104 | ::hypertext::Renderable::render_to(
105 | fn_name(#(
106 | #field_refs self.#field_names
107 | ),*),
108 | #buffer_ident,
109 | );
110 | }
111 | }
112 | };
113 |
114 | Ok(output)
115 | }
116 |
117 | fn to_pascal_case(s: &str) -> String {
118 | let mut result = String::new();
119 | let mut capitalize_next = true;
120 |
121 | for c in s.chars() {
122 | if c == '_' {
123 | capitalize_next = true;
124 | } else if capitalize_next {
125 | result.push(c.to_ascii_uppercase());
126 | capitalize_next = false;
127 | } else {
128 | result.push(c);
129 | }
130 | }
131 |
132 | result
133 | }
134 |
--------------------------------------------------------------------------------
/hypertext-macros/src/html/syntaxes/maud.rs:
--------------------------------------------------------------------------------
1 | use std::marker::PhantomData;
2 |
3 | use proc_macro2::Span;
4 | use syn::{
5 | Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, braced,
6 | ext::IdentExt,
7 | parse::{Parse, ParseStream},
8 | token::{Brace, Paren},
9 | };
10 |
11 | use crate::html::{
12 | Attribute, Component, Doctype, Element, ElementBody, ElementNode, Group, Syntax, UnquotedName,
13 | kw,
14 | };
15 |
16 | pub struct Maud;
17 |
18 | impl Syntax for Maud {}
19 |
20 | impl Parse for ElementNode {
21 | fn parse(input: ParseStream) -> syn::Result {
22 | let lookahead = input.lookahead1();
23 |
24 | if lookahead.peek(Ident::peek_any) {
25 | if input.fork().parse::()?.is_component() {
26 | input.parse().map(Self::Component)
27 | } else {
28 | input.parse().map(Self::Element)
29 | }
30 | } else if lookahead.peek(Token![!]) {
31 | input.parse().map(Self::Doctype)
32 | } else if lookahead.peek(LitStr)
33 | || lookahead.peek(LitInt)
34 | || lookahead.peek(LitBool)
35 | || lookahead.peek(LitFloat)
36 | || lookahead.peek(LitChar)
37 | {
38 | input.parse().map(Self::Literal)
39 | } else if lookahead.peek(Token![@]) {
40 | input.parse().map(Self::Control)
41 | } else if lookahead.peek(Paren) {
42 | input.parse().map(Self::Expr)
43 | } else if lookahead.peek(Token![%]) {
44 | input.parse().map(Self::DisplayExpr)
45 | } else if lookahead.peek(Token![?]) {
46 | input.parse().map(Self::DebugExpr)
47 | } else if lookahead.peek(Brace) {
48 | input.parse().map(Self::Group)
49 | } else {
50 | Err(lookahead.error())
51 | }
52 | }
53 | }
54 |
55 | impl Parse for Doctype {
56 | fn parse(input: ParseStream) -> syn::Result {
57 | Ok(Self {
58 | lt_token: Token),
59 | bang_token: input.parse()?,
60 | doctype_token: input.parse()?,
61 | html_token: kw::html(Span::mixed_site()),
62 | gt_token: Token),
63 | phantom: PhantomData,
64 | })
65 | }
66 | }
67 |
68 | impl Parse for Group> {
69 | fn parse(input: ParseStream) -> syn::Result {
70 | let content;
71 | braced!(content in input);
72 |
73 | Ok(Self(content.parse()?))
74 | }
75 | }
76 |
77 | impl Parse for Element {
78 | fn parse(input: ParseStream) -> syn::Result {
79 | Ok(Self {
80 | name: input.parse()?,
81 | attrs: {
82 | let mut attrs = Vec::new();
83 |
84 | if input.peek(Token![#]) {
85 | attrs.push(input.call(Attribute::parse_id)?);
86 | }
87 |
88 | if input.peek(Token![.]) {
89 | attrs.push(input.call(Attribute::parse_class_list)?);
90 | }
91 |
92 | while !(input.peek(Token![;]) || input.peek(Brace)) {
93 | attrs.push(input.parse()?);
94 | }
95 |
96 | attrs
97 | },
98 | body: input.parse()?,
99 | })
100 | }
101 | }
102 |
103 | impl Parse for ElementBody {
104 | fn parse(input: ParseStream) -> syn::Result {
105 | let lookahead = input.lookahead1();
106 |
107 | if lookahead.peek(Brace) {
108 | let content;
109 | braced!(content in input);
110 | content.parse().map(|children| Self::Normal {
111 | children,
112 | closing_name: None,
113 | })
114 | } else if lookahead.peek(Token![;]) {
115 | input.parse::().map(|_| Self::Void)
116 | } else {
117 | Err(lookahead.error())
118 | }
119 | }
120 | }
121 |
122 | impl Parse for Component {
123 | fn parse(input: ParseStream) -> syn::Result {
124 | Ok(Self {
125 | name: input.parse()?,
126 | attrs: {
127 | let mut attrs = Vec::new();
128 |
129 | while !(input.peek(Token![..]) || input.peek(Token![;]) || input.peek(Brace)) {
130 | attrs.push(input.parse()?);
131 | }
132 |
133 | attrs
134 | },
135 | dotdot: input.parse()?,
136 | body: input.parse()?,
137 | })
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/hypertext/src/validation/mathml.rs:
--------------------------------------------------------------------------------
1 | #![expect(missing_docs)]
2 |
3 | use crate::validation::{Attribute, Element};
4 |
5 | #[expect(non_upper_case_globals, clippy::doc_markdown)]
6 | /// Global MathML attributes.
7 | pub trait MathMlGlobalAttributes: Element {
8 | const autofocus: Attribute = Attribute;
9 |
10 | #[doc(alias = ".")]
11 | const class: Attribute = Attribute;
12 |
13 | const dir: Attribute = Attribute;
14 |
15 | const displaystyle: Attribute = Attribute;
16 |
17 | #[doc(alias = "#")]
18 | const id: Attribute = Attribute;
19 |
20 | const mathbackground: Attribute = Attribute;
21 |
22 | const mathcolor: Attribute = Attribute;
23 |
24 | const mathsize: Attribute = Attribute;
25 |
26 | const nonce: Attribute = Attribute;
27 |
28 | const scriptlevel: Attribute = Attribute;
29 |
30 | const style: Attribute = Attribute;
31 |
32 | const tabindex: Attribute = Attribute;
33 | }
34 |
35 | pub mod elements {
36 | macro_rules! elements {
37 | {
38 | $(
39 | $(#[$meta:meta])*
40 | $name:ident $(
41 | {
42 | $(
43 | $(#[$attr_meta:meta])*
44 | $attr:ident
45 | )*
46 | }
47 | )?
48 | )*
49 | } => {
50 | $(
51 | $(#[$meta])*
52 | #[expect(
53 | non_camel_case_types,
54 | reason = "camel case types will be interpreted as components"
55 | )]
56 | #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::marker::Copy)]
57 | pub struct $name;
58 |
59 | $(
60 | #[allow(non_upper_case_globals)]
61 | impl $name {
62 | $(
63 | $(#[$attr_meta])*
64 | pub const $attr: $crate::validation::Attribute = $crate::validation::Attribute;
65 | )*
66 | }
67 | )?
68 |
69 | impl $crate::validation::Element for $name {
70 | type Kind = $crate::validation::Normal;
71 | }
72 |
73 | impl super::MathMlGlobalAttributes for $name {}
74 | )*
75 | }
76 | }
77 |
78 | elements! {
79 | math {
80 | display
81 | }
82 |
83 | annotation {
84 | encoding
85 | }
86 |
87 | annotation_xml {
88 | encoding
89 | }
90 |
91 | menclose {
92 | notation
93 | }
94 |
95 | merror
96 |
97 | mfrac {
98 | linethickness
99 | }
100 |
101 | mi {
102 | mathvariant
103 | }
104 |
105 | mmultiscripts {
106 | mathvariant
107 | }
108 |
109 | mn
110 |
111 | mo {
112 | accent
113 | fence
114 | form
115 | largeop
116 | lspace
117 | maxsize
118 | minsize
119 | movablelimits
120 | rspace
121 | separator
122 | stretchy
123 | symmetric
124 | }
125 |
126 | mover {
127 | accent
128 | }
129 |
130 | mpadded {
131 | depth
132 | height
133 | lspace
134 | voffset
135 | width
136 | }
137 |
138 | mphantom
139 |
140 | mprescripts
141 |
142 | mroot
143 |
144 | mrow
145 |
146 | ms
147 |
148 | mspace {
149 | depth
150 | height
151 | width
152 | }
153 |
154 | msqrt
155 |
156 | mstyle
157 |
158 | msub
159 |
160 | msubsup
161 |
162 | msup
163 |
164 | mtable {
165 | align
166 | columnalign
167 | columnlines
168 | columnspacing
169 | frame
170 | framespacing
171 | rowalign
172 | rowlines
173 | rowspacing
174 | width
175 | }
176 |
177 | mtd {
178 | columnspan
179 | rowspan
180 | columnalign
181 | rowalign
182 | }
183 |
184 | mtext
185 |
186 | mtr {
187 | columnalign
188 | rowalign
189 | }
190 |
191 | munder {
192 | accentunder
193 | }
194 |
195 | munderover {
196 | accent
197 | accentunder
198 | }
199 |
200 | semantics
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/hypertext-macros/src/derive.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::{Span, TokenStream};
2 | use quote::quote;
3 | use syn::{DeriveInput, Error, spanned::Spanned};
4 |
5 | use crate::{
6 | AttributeValueNode, Context, Document, Maud, Nodes, Rsx,
7 | html::{self, generate::Generator},
8 | };
9 |
10 | #[allow(clippy::needless_pass_by_value)]
11 | pub fn renderable(input: DeriveInput) -> syn::Result {
12 | match (renderable_element(&input), attribute_renderable(&input)) {
13 | (Ok(None), Ok(None)) => Err(Error::new(
14 | Span::call_site(),
15 | "expected at least one of `maud`, `rsx`, or `attribute` attributes",
16 | )),
17 | (Ok(element), Ok(attribute)) => Ok(quote! {
18 | #element
19 | #attribute
20 | }),
21 | (Ok(_), Err(e)) | (Err(e), Ok(_)) => Err(e),
22 | (Err(mut e1), Err(e2)) => {
23 | e1.combine(e2);
24 | Err(e1)
25 | }
26 | }
27 | }
28 |
29 | fn renderable_element(input: &DeriveInput) -> syn::Result> {
30 | let mut attrs = input
31 | .attrs
32 | .iter()
33 | .filter_map(|attr| {
34 | if attr.path().is_ident("maud") {
35 | Some((
36 | attr,
37 | html::generate::lazy::>
38 | as fn(TokenStream, bool) -> syn::Result,
39 | ))
40 | } else if attr.path().is_ident("rsx") {
41 | Some((
42 | attr,
43 | html::generate::lazy::>
44 | as fn(TokenStream, bool) -> syn::Result,
45 | ))
46 | } else {
47 | None
48 | }
49 | })
50 | .peekable();
51 |
52 | let (lazy_fn, tokens) = match (attrs.next(), attrs.peek()) {
53 | (Some((attr, f)), None) => (f, attr.meta.require_list()?.tokens.clone()),
54 | (Some((attr, _)), Some(_)) => {
55 | let mut error = Error::new(
56 | attr.span(),
57 | "cannot have multiple `maud` or `rsx` attributes",
58 | );
59 | for (attr, _) in attrs {
60 | error.combine(syn::Error::new(
61 | attr.span(),
62 | "cannot have multiple `maud` or `rsx` attributes",
63 | ));
64 | }
65 | return Err(error);
66 | }
67 | (None, _) => {
68 | return Ok(None);
69 | }
70 | };
71 |
72 | let lazy = lazy_fn(tokens, true)?;
73 |
74 | let name = &input.ident;
75 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
76 | let buffer_ident = Generator::buffer_ident();
77 | let output = quote! {
78 | #[automatically_derived]
79 | impl #impl_generics ::hypertext::Renderable for #name #ty_generics #where_clause {
80 | fn render_to(&self, #buffer_ident: &mut ::hypertext::Buffer) {
81 | ::hypertext::Renderable::render_to(lazy, #buffer_ident);
82 | }
83 | }
84 | };
85 | Ok(Some(output))
86 | }
87 |
88 | fn attribute_renderable(input: &DeriveInput) -> syn::Result> {
89 | let mut attrs = input
90 | .attrs
91 | .iter()
92 | .filter(|attr| attr.path().is_ident("attribute"))
93 | .peekable();
94 |
95 | let tokens = match (attrs.next(), attrs.peek()) {
96 | (Some(attr), None) => attr.meta.require_list()?.tokens.clone(),
97 | (Some(_), Some(_)) => {
98 | let mut error = Error::new(
99 | Span::call_site(),
100 | "cannot have multiple `attribute` attributes",
101 | );
102 | for attr in attrs {
103 | error.combine(syn::Error::new(
104 | attr.span(),
105 | "cannot have multiple `attribute` attributes",
106 | ));
107 | }
108 | return Err(error);
109 | }
110 | (None, _) => {
111 | return Ok(None);
112 | }
113 | };
114 |
115 | let lazy = html::generate::lazy::>(tokens, true)?;
116 | let name = &input.ident;
117 | let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
118 | let buffer_ident = Generator::buffer_ident();
119 | let context_marker = Context::AttributeValue.marker_type();
120 | let output = quote! {
121 | #[automatically_derived]
122 | impl #impl_generics ::hypertext::Renderable<#context_marker> for #name #ty_generics
123 | #where_clause {
124 | fn render_to(
125 | &self,
126 | #buffer_ident: &mut ::hypertext::AttributeBuffer,
127 | ) {
128 | ::hypertext::Renderable::render_to(
129 | lazy,
130 | #buffer_ident,
131 | );
132 | }
133 | }
134 | };
135 |
136 | Ok(Some(output))
137 | }
138 |
--------------------------------------------------------------------------------
/hypertext/src/validation/mod.rs:
--------------------------------------------------------------------------------
1 | //! Types and traits used for validation of HTML elements and attributes.
2 | pub mod attributes;
3 | pub mod hypertext_elements;
4 | #[cfg(feature = "mathml")]
5 | mod mathml;
6 |
7 | /// A marker trait for type-checked elements.
8 | pub trait Element {
9 | /// The kind of this element.
10 | type Kind: ElementKind;
11 | }
12 | /// A marker trait to represent the kind of an element.
13 | ///
14 | /// This can be either [`Normal`] or [`Void`]. A [`Normal`] element will always
15 | /// have a closing tag, and can have children. A [`Void`] element will never
16 | /// have a closing tag, and cannot have children.
17 | pub trait ElementKind: sealed::Sealed {}
18 |
19 | /// A marker type to represent a normal element.
20 | ///
21 | /// This element has a closing tag and can have children.
22 | ///
23 | /// # Example
24 | ///
25 | /// ```html
26 | ///
27 | /// Hello, world!
28 | ///
29 | /// ```
30 | #[derive(Debug, Clone, Copy)]
31 | pub struct Normal;
32 |
33 | impl ElementKind for Normal {}
34 |
35 | /// A marker type to represent a void element.
36 | ///
37 | /// This element does not have a closing tag and cannot have children.
38 | ///
39 | /// # Example
40 | ///
41 | /// ```html
42 | ///
43 | /// ```
44 | #[derive(Debug, Clone, Copy)]
45 | pub struct Void;
46 |
47 | impl ElementKind for Void {}
48 |
49 | mod sealed {
50 | use super::{Normal, Void};
51 |
52 | pub trait Sealed {}
53 | impl Sealed for Normal {}
54 | impl Sealed for Void {}
55 | }
56 |
57 | /// A standard attribute.
58 | #[derive(Debug, Clone, Copy)]
59 | pub struct Attribute;
60 |
61 | /// An attribute namespace.
62 | #[derive(Debug, Clone, Copy)]
63 | pub struct AttributeNamespace;
64 |
65 | /// An attribute prefixed by a symbol.
66 | #[derive(Debug, Clone, Copy)]
67 | pub struct AttributeSymbol;
68 |
69 | /// Define custom elements.
70 | ///
71 | /// This macro should be called from within a module named `hypertext_elements`.
72 | ///
73 | /// # Example
74 | ///
75 | /// ```rust
76 | /// mod hypertext_elements {
77 | /// use hypertext::define_elements;
78 | /// // Re-export all standard HTML elements
79 | /// pub use hypertext::validation::hypertext_elements::*;
80 | ///
81 | /// define_elements! {
82 | /// /// A custom web component that greets the user.
83 | /// simple_greeting {
84 | /// /// The name of the person to greet.
85 | /// name
86 | /// }
87 | ///
88 | /// /// An element representing a coordinate.
89 | /// coordinate {
90 | /// /// The x coordinate.
91 | /// x
92 | ///
93 | /// /// The y coordinate.
94 | /// y
95 | /// }
96 | /// }
97 | /// }
98 | ///
99 | /// // Now, you can use the custom elements like this:
100 | ///
101 | /// use hypertext::prelude::*;
102 | ///
103 | /// assert_eq!(
104 | /// maud! {
105 | /// simple-greeting name="Alice" {
106 | /// coordinate x=1 y=2 {}
107 | /// }
108 | /// }
109 | /// .render()
110 | /// .as_inner(),
111 | /// r#" "#,
112 | /// )
113 | /// ```
114 | #[macro_export]
115 | macro_rules! define_elements {
116 | {
117 | $(
118 | $(#[$meta:meta])*
119 | $name:ident $(
120 | {
121 | $(
122 | $(#[$attr_meta:meta])*
123 | $attr:ident
124 | )*
125 | }
126 | )?
127 | )*
128 | } => {
129 | $(
130 | $(#[$meta])*
131 | #[expect(
132 | non_camel_case_types,
133 | reason = "camel case types will be interpreted as components"
134 | )]
135 | #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::marker::Copy)]
136 | pub struct $name;
137 |
138 | $(
139 | #[allow(non_upper_case_globals)]
140 | impl $name {
141 | $(
142 | $(#[$attr_meta])*
143 | pub const $attr: $crate::validation::Attribute = $crate::validation::Attribute;
144 | )*
145 | }
146 | )?
147 |
148 | impl $crate::validation::Element for $name {
149 | type Kind = $crate::validation::Normal;
150 | }
151 |
152 | impl $crate::validation::attributes::GlobalAttributes for $name {}
153 | )*
154 | }
155 | }
156 |
157 | /// Define custom void elements.
158 | ///
159 | /// This macro should be called from within a module named `hypertext_elements`.
160 | ///
161 | /// # Example
162 | /// ```rust
163 | /// mod hypertext_elements {
164 | /// // Re-export all standard HTML elements
165 | /// use hypertext::define_void_elements;
166 | /// pub use hypertext::validation::hypertext_elements::*;
167 | ///
168 | /// define_void_elements! {
169 | /// /// A custom void element that greets the user.
170 | /// simple_greeting {
171 | /// /// The name of the person to greet.
172 | /// name
173 | /// }
174 | /// }
175 | /// }
176 | ///
177 | /// // Now, you can use the custom elements like this:
178 | ///
179 | /// use hypertext::prelude::*;
180 | ///
181 | /// assert_eq!(
182 | /// maud! {
183 | /// simple-greeting name="Alice";
184 | /// }
185 | /// .render()
186 | /// .as_inner(),
187 | /// r#""#,
188 | /// )
189 | /// ```
190 | #[macro_export]
191 | macro_rules! define_void_elements {
192 | {
193 | $(
194 | $(#[$meta:meta])*
195 | $name:ident $(
196 | {
197 | $(
198 | $(#[$attr_meta:meta])*
199 | $attr:ident
200 | )*
201 | }
202 | )?
203 | )*
204 | } => {
205 | $(
206 | $(#[$meta])*
207 | #[expect(
208 | non_camel_case_types,
209 | reason = "camel case types will be interpreted as components"
210 | )]
211 | #[derive(::core::fmt::Debug, ::core::clone::Clone, ::core::marker::Copy)]
212 | pub struct $name;
213 |
214 | $(
215 | #[allow(non_upper_case_globals)]
216 | impl $name {
217 | $(
218 | $(#[$attr_meta])*
219 | pub const $attr: $crate::validation::Attribute = $crate::validation::Attribute;
220 | )*
221 | }
222 | )?
223 |
224 | impl $crate::validation::Element for $name {
225 | type Kind = $crate::validation::Void;
226 | }
227 |
228 | impl $crate::validation::attributes::GlobalAttributes for $name {}
229 | )*
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/hypertext/src/web_frameworks.rs:
--------------------------------------------------------------------------------
1 | extern crate alloc;
2 |
3 | #[allow(unused_imports)]
4 | use alloc::string::String;
5 |
6 | #[allow(dead_code)]
7 | const HTML_CONTENT_TYPE: &str = "text/html; charset=utf-8";
8 |
9 | #[cfg(feature = "actix-web")]
10 | mod actix_web {
11 | use actix_web::{HttpRequest, HttpResponse, Responder, web::Html};
12 |
13 | use super::String;
14 | use crate::{Buffer, Lazy, Renderable, Rendered};
15 |
16 | impl Responder for Lazy {
17 | type Body = as Responder>::Body;
18 |
19 | #[inline]
20 | fn respond_to(self, req: &HttpRequest) -> HttpResponse {
21 | self.render().respond_to(req)
22 | }
23 | }
24 |
25 | impl> Responder for Rendered {
26 | type Body = ::Body;
27 |
28 | #[inline]
29 | fn respond_to(self, req: &HttpRequest) -> HttpResponse {
30 | Html::new(self.0).respond_to(req)
31 | }
32 | }
33 | }
34 |
35 | #[cfg(feature = "axum")]
36 | mod axum {
37 | use axum_core::response::{IntoResponse, Response};
38 |
39 | use super::String;
40 | use crate::{Buffer, Lazy, Renderable, Rendered};
41 |
42 | const CONTENT_TYPE_HEADER: &str = "content-type";
43 |
44 | impl IntoResponse for Lazy {
45 | #[inline]
46 | fn into_response(self) -> Response {
47 | self.render().into_response()
48 | }
49 | }
50 |
51 | impl IntoResponse for Rendered<&'static str> {
52 | #[inline]
53 | fn into_response(self) -> Response {
54 | ([(CONTENT_TYPE_HEADER, super::HTML_CONTENT_TYPE)], self.0).into_response()
55 | }
56 | }
57 |
58 | impl IntoResponse for Rendered {
59 | #[inline]
60 | fn into_response(self) -> Response {
61 | ([(CONTENT_TYPE_HEADER, super::HTML_CONTENT_TYPE)], self.0).into_response()
62 | }
63 | }
64 | }
65 |
66 | #[cfg(feature = "ntex")]
67 | mod ntex {
68 | #![allow(clippy::future_not_send)]
69 |
70 | use ntex::{
71 | http::Response,
72 | web::{ErrorRenderer, HttpRequest, Responder},
73 | };
74 |
75 | use super::String;
76 | use crate::{Buffer, Lazy, Renderable, Rendered};
77 |
78 | impl Responder for Lazy {
79 | #[inline]
80 | async fn respond_to(self, req: &HttpRequest) -> Response {
81 | Responder::::respond_to(self.render(), req).await
82 | }
83 | }
84 |
85 | impl Responder for Rendered<&'static str> {
86 | #[inline]
87 | async fn respond_to(self, _: &HttpRequest) -> Response {
88 | Response::Ok()
89 | .content_type(super::HTML_CONTENT_TYPE)
90 | .body(self.0)
91 | }
92 | }
93 |
94 | impl Responder for Rendered {
95 | #[inline]
96 | async fn respond_to(self, _: &HttpRequest) -> Response {
97 | Response::Ok()
98 | .content_type(super::HTML_CONTENT_TYPE)
99 | .body(self.0)
100 | }
101 | }
102 | }
103 |
104 | #[cfg(feature = "poem")]
105 | mod poem {
106 | use core::marker::Send;
107 |
108 | use poem::{IntoResponse, Response, web::Html};
109 |
110 | use super::String;
111 | use crate::{Buffer, Lazy, Renderable, Rendered};
112 |
113 | impl IntoResponse for Lazy {
114 | #[inline]
115 | fn into_response(self) -> Response {
116 | self.render().into_response()
117 | }
118 | }
119 |
120 | impl + Send> IntoResponse for Rendered {
121 | #[inline]
122 | fn into_response(self) -> Response {
123 | Html(self.0).into_response()
124 | }
125 | }
126 | }
127 |
128 | #[cfg(feature = "rocket")]
129 | mod rocket {
130 | use rocket::{
131 | Request,
132 | response::{Responder, Result, content::RawHtml},
133 | };
134 |
135 | use super::String;
136 | use crate::{Buffer, Lazy, Renderable, Rendered};
137 |
138 | impl<'r, 'o: 'r, F: Fn(&mut Buffer) + Send> Responder<'r, 'o> for Lazy {
139 | #[inline]
140 | fn respond_to(self, req: &'r Request<'_>) -> Result<'o> {
141 | self.render().respond_to(req)
142 | }
143 | }
144 |
145 | impl<'r, 'o: 'r> Responder<'r, 'o> for Rendered<&'o str> {
146 | #[inline]
147 | fn respond_to(self, req: &'r Request<'_>) -> Result<'o> {
148 | RawHtml(self.0).respond_to(req)
149 | }
150 | }
151 |
152 | impl<'r, 'o: 'r> Responder<'r, 'o> for Rendered {
153 | #[inline]
154 | fn respond_to(self, req: &'r Request<'_>) -> Result<'o> {
155 | RawHtml(self.0).respond_to(req)
156 | }
157 | }
158 | }
159 |
160 | #[cfg(feature = "salvo")]
161 | mod salvo {
162 | use salvo_core::{Response, Scribe, writing::Text};
163 |
164 | use super::String;
165 | use crate::{Buffer, Lazy, Renderable, Rendered};
166 |
167 | impl Scribe for Lazy {
168 | #[inline]
169 | fn render(self, res: &mut Response) {
170 | Renderable::render(&self).render(res);
171 | }
172 | }
173 |
174 | impl Scribe for Rendered<&'static str> {
175 | #[inline]
176 | fn render(self, res: &mut Response) {
177 | Text::Html(self.0).render(res);
178 | }
179 | }
180 |
181 | impl Scribe for Rendered {
182 | #[inline]
183 | fn render(self, res: &mut Response) {
184 | Text::Html(self.0).render(res);
185 | }
186 | }
187 | }
188 |
189 | #[cfg(feature = "tide")]
190 | mod tide {
191 |
192 | use tide::{Response, http::mime};
193 |
194 | use super::String;
195 | use crate::{Buffer, Lazy, Renderable, Rendered};
196 |
197 | impl From> for Response {
198 | #[inline]
199 | fn from(lazy: Lazy) -> Self {
200 | lazy.render().into()
201 | }
202 | }
203 |
204 | impl> From> for Response {
205 | #[inline]
206 | fn from(Rendered(value): Rendered) -> Self {
207 | let mut resp = Self::from(value.into());
208 | resp.set_content_type(mime::HTML);
209 | resp
210 | }
211 | }
212 | }
213 |
214 | #[cfg(feature = "warp")]
215 | mod warp {
216 | use warp::reply::{self, Reply, Response};
217 |
218 | use super::String;
219 | use crate::{Buffer, Lazy, Renderable, Rendered};
220 |
221 | impl Reply for Lazy {
222 | #[inline]
223 | fn into_response(self) -> Response {
224 | self.render().into_response()
225 | }
226 | }
227 |
228 | impl Reply for Rendered<&'static str> {
229 | #[inline]
230 | fn into_response(self) -> Response {
231 | reply::html(self.0).into_response()
232 | }
233 | }
234 |
235 | impl Reply for Rendered {
236 | #[inline]
237 | fn into_response(self) -> Response {
238 | reply::html(self.0).into_response()
239 | }
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/hypertext/src/macros/alloc.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::doc_markdown)]
2 | /// Derive [`Renderable`](crate::Renderable) for a type.
3 | ///
4 | /// # Examples
5 | ///
6 | /// ## [`maud!`]
7 | ///
8 | /// ```
9 | /// use hypertext::prelude::*;
10 | ///
11 | /// #[derive(Renderable)]
12 | /// #[maud(span { "My name is " (self.name) "!" })]
13 | /// pub struct Person {
14 | /// name: String,
15 | /// }
16 | ///
17 | /// assert_eq!(
18 | /// maud! { div { (Person { name: "Alice".into() }) } }
19 | /// .render()
20 | /// .as_inner(),
21 | /// "My name is Alice!
"
22 | /// );
23 | /// ```
24 | ///
25 | /// ## [`rsx!`]
26 | ///
27 | /// ```
28 | /// use hypertext::prelude::*;
29 | ///
30 | /// #[derive(Renderable)]
31 | /// #[rsx(
32 | /// "My name is " (self.name) "!"
33 | /// )]
34 | /// pub struct Person {
35 | /// name: String,
36 | /// }
37 | ///
38 | /// assert_eq!(
39 | /// rsx! { (Person { name: "Alice".into() })
}
40 | /// .render()
41 | /// .as_inner(),
42 | /// "My name is Alice!
"
43 | /// );
44 | /// ```
45 | ///
46 | /// ## [`attribute!`]
47 | ///
48 | /// ```
49 | /// use hypertext::prelude::*;
50 | ///
51 | /// #[derive(Renderable)]
52 | /// #[attribute((self.x) "," (self.y))]
53 | /// pub struct Coordinates {
54 | /// x: i32,
55 | /// y: i32,
56 | /// }
57 | ///
58 | /// assert_eq!(
59 | /// maud! { div title=(Coordinates { x: 10, y: 20 }) { "Location" } }
60 | /// .render()
61 | /// .as_inner(),
62 | /// r#"Location
"#
63 | /// );
64 | /// ```
65 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
66 | pub use hypertext_macros::Renderable;
67 | /// Generate an attribute value, returning a
68 | /// [`LazyAttribute`](crate::LazyAttribute).
69 | ///
70 | /// # Example
71 | ///
72 | /// ```
73 | /// use hypertext::prelude::*;
74 | ///
75 | /// let attr = attribute! { "x" @for i in 0..5 { (i) } };
76 | ///
77 | /// assert_eq!(
78 | /// maud! { div title=attr { "Hi!" } }.render().as_inner(),
79 | /// "Hi!
"
80 | /// );
81 | /// ```
82 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
83 | pub use hypertext_macros::attribute;
84 | /// Generate an attribute value, borrowing the environment.
85 | ///
86 | /// This is identical to [`attribute!`], except that it does not take
87 | /// ownership of the environment. This is useful when you want to build
88 | /// a [`LazyAttribute`] using some captured variables, but you still
89 | /// want to be able to use the variables after the [`LazyAttribute`] is
90 | /// created.
91 | ///
92 | /// [`LazyAttribute`]: crate::LazyAttribute
93 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
94 | pub use hypertext_macros::attribute_borrow;
95 | /// Convert a function returning a [`Renderable`](crate::Renderable) into a
96 | /// component.
97 | ///
98 | /// This is a procedural macro that takes a function and generates a
99 | /// struct that holds the function's parameters. The struct implements
100 | /// [`Renderable`] and can be used as a component.
101 | ///
102 | /// There are three types of parameters that are supported:
103 | /// - `T`: Stored as `T` in the struct, and will use [`Copy`] to provide the
104 | /// value to the function.
105 | /// - `&T`: Stored as `T` in the struct, and will borrow the value from the
106 | /// struct when calling the function.
107 | /// - `&'a T`: Stored as `&'a T` in the struct, useful for borrowing unsized
108 | /// types such as [`str`] or [`[T]`](slice) without needing to convert them to
109 | /// their owned counterparts.
110 | ///
111 | /// The name of the generated struct is derived from the function name by
112 | /// converting it to PascalCase. If you would like to set a different name,
113 | /// you can specify it as `#[component(MyComponentName)]` on the function.
114 | ///
115 | /// The visibility of the generated struct is determined by the visibility
116 | /// of the function. If you would like to set a different visibility,
117 | /// you can specify it as `#[component(pub)]`,
118 | /// `#[component(pub(crate))]`, etc. on the function.
119 | ///
120 | /// You can combine both of these by setting an attribute like
121 | /// `#[component(pub MyComponentName)]`.
122 | ///
123 | /// # Example
124 | ///
125 | /// ```
126 | /// use hypertext::prelude::*;
127 | ///
128 | /// #[component]
129 | /// fn nav_bar<'a>(title: &'a str, subtitle: &String) -> impl Renderable {
130 | /// maud! {
131 | /// nav {
132 | /// h1 { (title) }
133 | /// h2 { (subtitle) }
134 | /// }
135 | /// }
136 | /// }
137 | ///
138 | /// assert_eq!(
139 | /// maud! {
140 | /// div {
141 | /// NavBar title="My Nav Bar" subtitle=("My Subtitle".to_owned());
142 | /// }
143 | /// }
144 | /// .render()
145 | /// .as_inner(),
146 | /// "My Nav Bar My Subtitle
",
147 | /// );
148 | /// ```
149 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
150 | pub use hypertext_macros::component;
151 | /// Generate HTML using [`maud`] syntax, returning a [`Lazy`](crate::Lazy).
152 | ///
153 | /// Note that this is not a complete 1:1 port of [`maud`]'s syntax as it is
154 | /// stricter in some places to prevent anti-patterns.
155 | ///
156 | /// Some key differences are:
157 | /// - Attribute keys must be simple punctuation-separated identifiers.
158 | /// - [`id`]'s shorthand (`#`), if specified, must be the first attribute.
159 | /// - [`class`]'s shorthand (`.`), if specified must be the second group of
160 | /// attributes.
161 | ///
162 | /// Additionally, adding `!DOCTYPE` at the beginning of the invocation will
163 | /// render `""`.
164 | ///
165 | /// For more details, see the [maud book](https://maud.lambda.xyz).
166 | ///
167 | /// # Example
168 | ///
169 | /// ```
170 | /// use hypertext::prelude::*;
171 | ///
172 | /// assert_eq!(
173 | /// maud! {
174 | /// div #profile title="Profile" {
175 | /// h1 { "Alice" }
176 | /// }
177 | /// }
178 | /// .render()
179 | /// .as_inner(),
180 | /// r#"
Alice "#
181 | /// );
182 | /// ```
183 | ///
184 | /// [`maud`]: https://docs.rs/maud
185 | /// [`id`]: crate::validation::GlobalAttributes::id
186 | /// [`class`]: crate::validation::GlobalAttributes::class
187 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
188 | pub use hypertext_macros::maud;
189 | /// Generate HTML using [`maud!`] syntax, borrowing the environment.
190 | ///
191 | /// This is identical to [`maud!`], except that it does not take ownership
192 | /// of the environment. This is useful when you want to build a [`Lazy`]
193 | /// using some captured variables, but you still want to be able to use
194 | /// the variables after the [`Lazy`] is created.
195 | ///
196 | /// [`Lazy`]: crate::Lazy
197 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
198 | pub use hypertext_macros::maud_borrow;
199 | /// Generate HTML using rsx syntax, returning a [`Lazy`](crate::Lazy).
200 | ///
201 | /// # Example
202 | ///
203 | /// ```
204 | /// use hypertext::prelude::*;
205 | ///
206 | /// assert_eq!(
207 | /// rsx! {
208 | ///
209 | ///
Alice
210 | ///
211 | /// }
212 | /// .render()
213 | /// .as_inner(),
214 | /// r#"
Alice "#
215 | /// );
216 | /// ```
217 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
218 | pub use hypertext_macros::rsx;
219 | /// Generate HTML using [`rsx!`] syntax, borrowing the environment.
220 | ///
221 | /// This is identical to [`rsx!`], except that it does not take ownership of
222 | /// the environment. This is useful when you want to build a [`Lazy`] using
223 | /// some captured variables, but you still want to be able to use the
224 | /// variables after the [`Lazy`] is created.
225 | ///
226 | /// [`Lazy`]: crate::Lazy
227 | #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
228 | pub use hypertext_macros::rsx_borrow;
229 |
--------------------------------------------------------------------------------
/hypertext-macros/src/html/syntaxes/rsx.rs:
--------------------------------------------------------------------------------
1 | use std::marker::PhantomData;
2 |
3 | use syn::{
4 | Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, custom_punctuation,
5 | ext::IdentExt,
6 | parse::{Parse, ParseStream, discouraged::Speculative},
7 | parse_quote,
8 | token::Paren,
9 | };
10 |
11 | use crate::html::{
12 | Component, Doctype, Element, ElementBody, ElementNode, Group, Literal, Nodes, Syntax,
13 | UnquotedName,
14 | };
15 |
16 | pub struct Rsx;
17 |
18 | impl Syntax for Rsx {}
19 |
20 | custom_punctuation!(FragmentOpen, <>);
21 | custom_punctuation!(FragmentClose, >);
22 | custom_punctuation!(OpenTagSolidusEnd, />);
23 | custom_punctuation!(CloseTagStart, );
24 |
25 | impl ElementNode {
26 | fn parse_component(input: ParseStream) -> syn::Result {
27 | input.parse::()?;
28 |
29 | let name = input.parse::()?;
30 |
31 | let mut attrs = Vec::new();
32 |
33 | while !(input.peek(Token![..]) || input.peek(Token![>]) || input.peek(OpenTagSolidusEnd)) {
34 | attrs.push(input.parse()?);
35 | }
36 |
37 | let dotdot = input.parse()?;
38 |
39 | let solidus = input.parse::>()?;
40 | input.parse::]>()?;
41 |
42 | if solidus.is_some() {
43 | Ok(Self::Component(Component {
44 | name,
45 | attrs,
46 | dotdot,
47 | body: ElementBody::Void,
48 | }))
49 | } else {
50 | let mut children = Vec::new();
51 |
52 | while !input.peek(CloseTagStart) {
53 | if input.is_empty() {
54 | children.insert(
55 | 0,
56 | Self::Component(Component {
57 | name,
58 | attrs,
59 | dotdot,
60 | body: ElementBody::Void,
61 | }),
62 | );
63 |
64 | return Ok(Self::Group(Group(Nodes(children))));
65 | }
66 |
67 | children.push(input.parse()?);
68 | }
69 |
70 | let fork = input.fork();
71 | fork.parse::()?;
72 | let closing_name = fork.parse::()?;
73 | if closing_name == name {
74 | input.advance_to(&fork);
75 | } else {
76 | children.insert(
77 | 0,
78 | Self::Component(Component {
79 | name,
80 | attrs,
81 | dotdot,
82 | body: ElementBody::Void,
83 | }),
84 | );
85 |
86 | return Ok(Self::Group(Group(Nodes(children))));
87 | }
88 | input.parse::]>()?;
89 |
90 | Ok(Self::Component(Component {
91 | name,
92 | attrs,
93 | dotdot,
94 | body: ElementBody::Normal {
95 | children: Nodes(children),
96 | closing_name: Some(parse_quote!(#closing_name)),
97 | },
98 | }))
99 | }
100 | }
101 |
102 | fn parse_element(input: ParseStream) -> syn::Result {
103 | input.parse::()?;
104 |
105 | let name = input.parse()?;
106 |
107 | let mut attrs = Vec::new();
108 |
109 | while !(input.peek(Token![>]) || (input.peek(OpenTagSolidusEnd))) {
110 | attrs.push(input.parse()?);
111 | }
112 |
113 | let solidus = input.parse::>()?;
114 | input.parse::]>()?;
115 |
116 | if solidus.is_some() {
117 | Ok(Self::Element(Element {
118 | name,
119 | attrs,
120 | body: ElementBody::Void,
121 | }))
122 | } else {
123 | let mut children = Vec::new();
124 |
125 | while !(input.peek(CloseTagStart)) {
126 | if input.is_empty() {
127 | children.insert(
128 | 0,
129 | Self::Element(Element {
130 | name,
131 | attrs,
132 | body: ElementBody::Void,
133 | }),
134 | );
135 |
136 | return Ok(Self::Group(Group(Nodes(children))));
137 | }
138 | children.push(input.parse()?);
139 | }
140 |
141 | let fork = input.fork();
142 | fork.parse::()?;
143 | let closing_name = fork.parse()?;
144 | if closing_name == name {
145 | input.advance_to(&fork);
146 | } else {
147 | children.insert(
148 | 0,
149 | Self::Element(Element {
150 | name,
151 | attrs,
152 | body: ElementBody::Void,
153 | }),
154 | );
155 |
156 | return Ok(Self::Group(Group(Nodes(children))));
157 | }
158 | input.parse::]>()?;
159 |
160 | Ok(Self::Element(Element {
161 | name,
162 | attrs,
163 | body: ElementBody::Normal {
164 | children: Nodes(children),
165 | closing_name: Some(closing_name),
166 | },
167 | }))
168 | }
169 | }
170 | }
171 |
172 | impl Parse for ElementNode {
173 | fn parse(input: ParseStream) -> syn::Result {
174 | let lookahead = input.lookahead1();
175 |
176 | if lookahead.peek(Token![<]) {
177 | let fork = input.fork();
178 | fork.parse::()?;
179 | let lookahead = fork.lookahead1();
180 | if lookahead.peek(Token![>]) {
181 | input.parse().map(Self::Group)
182 | } else if lookahead.peek(Ident::peek_any) {
183 | if fork.parse::()?.is_component() {
184 | input.call(Self::parse_component)
185 | } else {
186 | input.call(Self::parse_element)
187 | }
188 | } else if lookahead.peek(Token![!]) {
189 | input.parse().map(Self::Doctype)
190 | } else {
191 | Err(lookahead.error())
192 | }
193 | } else if lookahead.peek(Token![@]) {
194 | input.parse().map(Self::Control)
195 | } else if lookahead.peek(Paren) {
196 | input.parse().map(Self::Expr)
197 | } else if lookahead.peek(Token![%]) {
198 | input.parse().map(Self::DisplayExpr)
199 | } else if lookahead.peek(Token![?]) {
200 | input.parse().map(Self::DebugExpr)
201 | } else if lookahead.peek(LitStr)
202 | || lookahead.peek(LitInt)
203 | || lookahead.peek(LitBool)
204 | || lookahead.peek(LitFloat)
205 | || lookahead.peek(LitChar)
206 | {
207 | input.parse().map(Self::Literal)
208 | } else if lookahead.peek(Ident::peek_any) {
209 | let ident = input.call(Ident::parse_any)?;
210 |
211 | let ident_string = if input.peek(Ident::peek_any)
212 | || input.peek(LitInt)
213 | || input.peek(LitBool)
214 | || input.peek(LitFloat)
215 | {
216 | format!("{ident} ")
217 | } else {
218 | ident.to_string()
219 | };
220 |
221 | Ok(Self::Literal(Literal::Str(LitStr::new(
222 | &ident_string,
223 | ident.span(),
224 | ))))
225 | } else {
226 | Err(lookahead.error())
227 | }
228 | }
229 | }
230 |
231 | impl Parse for Doctype {
232 | fn parse(input: ParseStream) -> syn::Result {
233 | Ok(Self {
234 | lt_token: input.parse()?,
235 | bang_token: input.parse()?,
236 | doctype_token: input.parse()?,
237 | html_token: input.parse()?,
238 | gt_token: input.parse()?,
239 | phantom: PhantomData,
240 | })
241 | }
242 | }
243 |
244 | impl Parse for Group> {
245 | fn parse(input: ParseStream) -> syn::Result {
246 | input.parse::()?;
247 |
248 | let mut nodes = Vec::new();
249 |
250 | while !input.peek(FragmentClose) {
251 | nodes.push(input.parse()?);
252 | }
253 |
254 | input.parse::()?;
255 |
256 | Ok(Self(Nodes(nodes)))
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/hypertext-macros/src/html/control.rs:
--------------------------------------------------------------------------------
1 | use proc_macro2::TokenStream;
2 | use quote::{ToTokens, quote};
3 | use syn::{
4 | Expr, Local, Pat, Stmt, Token, braced,
5 | parse::{Parse, ParseStream},
6 | token::Brace,
7 | };
8 |
9 | use super::{AnyBlock, Generate, Generator, Node, Nodes};
10 | use crate::Context;
11 |
12 | pub enum Control {
13 | Let(Let),
14 | If(If),
15 | For(For),
16 | While(While),
17 | Match(Match),
18 | }
19 |
20 | impl Parse for Control {
21 | fn parse(input: ParseStream) -> syn::Result {
22 | input.parse::()?;
23 |
24 | let lookahead = input.lookahead1();
25 |
26 | if lookahead.peek(Token![let]) {
27 | input.parse().map(Self::Let)
28 | } else if lookahead.peek(Token![if]) {
29 | input.parse().map(Self::If)
30 | } else if lookahead.peek(Token![for]) {
31 | input.parse().map(Self::For)
32 | } else if lookahead.peek(Token![while]) {
33 | input.parse().map(Self::While)
34 | } else if lookahead.peek(Token![match]) {
35 | input.parse().map(Self::Match)
36 | } else {
37 | Err(lookahead.error())
38 | }
39 | }
40 | }
41 |
42 | impl Generate for Control {
43 | const CONTEXT: Context = N::CONTEXT;
44 |
45 | fn generate(&self, g: &mut Generator) {
46 | match self {
47 | Self::Let(let_) => g.push(let_),
48 | Self::If(if_) => g.push(if_),
49 | Self::For(for_) => g.push(for_),
50 | Self::While(while_) => g.push(while_),
51 | Self::Match(match_) => g.push(match_),
52 | }
53 | }
54 | }
55 |
56 | pub struct Let(Local);
57 |
58 | impl Parse for Let {
59 | fn parse(input: ParseStream) -> syn::Result {
60 | let local = match input.parse()? {
61 | Stmt::Local(local) => local,
62 | stmt => return Err(syn::Error::new_spanned(stmt, "expected `let` statement")),
63 | };
64 |
65 | Ok(Self(local))
66 | }
67 | }
68 |
69 | impl Generate for Let {
70 | const CONTEXT: Context = Context::Node;
71 |
72 | fn generate(&self, g: &mut Generator) {
73 | g.push_stmt(&self.0);
74 | }
75 | }
76 |
77 | pub struct ControlBlock {
78 | brace_token: Brace,
79 | nodes: Nodes,
80 | }
81 |
82 | impl ControlBlock {
83 | fn block(&self, g: &mut Generator) -> AnyBlock {
84 | self.nodes.block(g, self.brace_token)
85 | }
86 | }
87 |
88 | impl Parse for ControlBlock {
89 | fn parse(input: ParseStream) -> syn::Result {
90 | let content;
91 |
92 | Ok(Self {
93 | brace_token: braced!(content in input),
94 | nodes: content.parse()?,
95 | })
96 | }
97 | }
98 |
99 | pub struct If {
100 | if_token: Token![if],
101 | cond: Expr,
102 | then_block: ControlBlock,
103 | else_branch: Option<(Token![else], Box>)>,
104 | }
105 |
106 | impl Parse for If {
107 | fn parse(input: ParseStream) -> syn::Result {
108 | Ok(Self {
109 | if_token: input.parse()?,
110 | cond: input.call(Expr::parse_without_eager_brace)?,
111 | then_block: input.parse()?,
112 | else_branch: if input.peek(Token![@]) && input.peek2(Token![else]) {
113 | input.parse::()?;
114 |
115 | Some((input.parse()?, input.parse()?))
116 | } else {
117 | None
118 | },
119 | })
120 | }
121 | }
122 |
123 | impl Generate for If {
124 | const CONTEXT: Context = N::CONTEXT;
125 |
126 | fn generate(&self, g: &mut Generator) {
127 | fn to_expr(if_: &If, g: &mut Generator) -> TokenStream {
128 | let if_token = if_.if_token;
129 | let cond = &if_.cond;
130 | let then_block = if_.then_block.block(g);
131 | let else_branch = if_.else_branch.as_ref().map(|(else_token, if_or_block)| {
132 | let else_block = match &**if_or_block {
133 | ControlIfOrBlock::If(if_) => to_expr(if_, g),
134 | ControlIfOrBlock::Block(block) => block.block(g).to_token_stream(),
135 | };
136 |
137 | quote! {
138 | #else_token #else_block
139 | }
140 | });
141 |
142 | quote! {
143 | #if_token #cond
144 | #then_block
145 | #else_branch
146 | }
147 | }
148 |
149 | let expr = to_expr(self, g);
150 |
151 | g.push_stmt(expr);
152 | }
153 | }
154 |
155 | pub enum ControlIfOrBlock {
156 | If(If),
157 | Block(ControlBlock),
158 | }
159 |
160 | impl Parse for ControlIfOrBlock {
161 | fn parse(input: ParseStream) -> syn::Result {
162 | let lookahead = input.lookahead1();
163 |
164 | if lookahead.peek(Token![if]) {
165 | input.parse().map(Self::If)
166 | } else if lookahead.peek(Brace) {
167 | input.parse().map(Self::Block)
168 | } else {
169 | Err(lookahead.error())
170 | }
171 | }
172 | }
173 |
174 | pub struct For {
175 | for_token: Token![for],
176 | pat: Pat,
177 | in_token: Token![in],
178 | expr: Expr,
179 | block: ControlBlock,
180 | }
181 |
182 | impl Parse for For {
183 | fn parse(input: ParseStream) -> syn::Result {
184 | Ok(Self {
185 | for_token: input.parse()?,
186 | pat: input.call(Pat::parse_multi_with_leading_vert)?,
187 | in_token: input.parse()?,
188 | expr: input.call(Expr::parse_without_eager_brace)?,
189 | block: input.parse()?,
190 | })
191 | }
192 | }
193 |
194 | impl Generate for For {
195 | const CONTEXT: Context = N::CONTEXT;
196 |
197 | fn generate(&self, g: &mut Generator) {
198 | let for_token = self.for_token;
199 | let pat = &self.pat;
200 | let in_token = self.in_token;
201 | let expr = &self.expr;
202 | let block = self.block.block(g);
203 |
204 | g.push_stmt(quote! {
205 | #for_token #pat #in_token #expr
206 | #block
207 | });
208 | }
209 | }
210 |
211 | pub struct While {
212 | while_token: Token![while],
213 | cond: Expr,
214 | block: ControlBlock,
215 | }
216 |
217 | impl Parse for While {
218 | fn parse(input: ParseStream) -> syn::Result {
219 | Ok(Self {
220 | while_token: input.parse()?,
221 | cond: input.call(Expr::parse_without_eager_brace)?,
222 | block: input.parse()?,
223 | })
224 | }
225 | }
226 |
227 | impl Generate for While {
228 | const CONTEXT: Context = N::CONTEXT;
229 |
230 | fn generate(&self, g: &mut Generator) {
231 | let while_token = self.while_token;
232 | let cond = &self.cond;
233 | let block = self.block.block(g);
234 |
235 | g.push_stmt(quote! {
236 | #while_token #cond
237 | #block
238 | });
239 | }
240 | }
241 |
242 | pub struct Match {
243 | match_token: Token![match],
244 | expr: Expr,
245 | brace_token: Brace,
246 | arms: Vec>,
247 | }
248 |
249 | impl Parse for Match {
250 | fn parse(input: ParseStream) -> syn::Result {
251 | let content;
252 |
253 | Ok(Self {
254 | match_token: input.parse()?,
255 | expr: input.call(Expr::parse_without_eager_brace)?,
256 | brace_token: braced!(content in input),
257 | arms: {
258 | let mut arms = Vec::new();
259 |
260 | while !content.is_empty() {
261 | arms.push(content.parse()?);
262 | }
263 |
264 | arms
265 | },
266 | })
267 | }
268 | }
269 |
270 | impl Generate for Match {
271 | const CONTEXT: Context = N::CONTEXT;
272 |
273 | fn generate(&self, g: &mut Generator) {
274 | let arms = self
275 | .arms
276 | .iter()
277 | .map(|arm| {
278 | let pat = arm.pat.clone();
279 | let guard = arm
280 | .guard
281 | .as_ref()
282 | .map(|(if_token, guard)| quote!(#if_token #guard));
283 | let fat_arrow_token = arm.fat_arrow_token;
284 | let block = match &arm.body {
285 | MatchNodeArmBody::Block(block) => block.block(g),
286 | MatchNodeArmBody::Node(node) => {
287 | g.block_with(Brace::default(), |g| g.push(node))
288 | }
289 | };
290 | let comma = arm.comma_token;
291 |
292 | quote!(#pat #guard #fat_arrow_token #block #comma)
293 | })
294 | .collect::();
295 |
296 | let match_token = self.match_token;
297 | let expr = &self.expr;
298 |
299 | let mut stmt = quote!(#match_token #expr);
300 |
301 | self.brace_token
302 | .surround(&mut stmt, |tokens| tokens.extend(arms));
303 |
304 | g.push_stmt(stmt);
305 | }
306 | }
307 |
308 | pub struct MatchNodeArm {
309 | pat: Pat,
310 | guard: Option<(Token![if], Expr)>,
311 | fat_arrow_token: Token![=>],
312 | body: MatchNodeArmBody,
313 | comma_token: Option,
314 | }
315 |
316 | impl Parse for MatchNodeArm {
317 | fn parse(input: ParseStream) -> syn::Result {
318 | Ok(Self {
319 | pat: input.call(Pat::parse_multi_with_leading_vert)?,
320 | guard: if input.peek(Token![if]) {
321 | Some((input.parse()?, input.parse()?))
322 | } else {
323 | None
324 | },
325 | fat_arrow_token: input.parse()?,
326 | body: input.parse()?,
327 | comma_token: input.parse()?,
328 | })
329 | }
330 | }
331 |
332 | pub enum MatchNodeArmBody {
333 | Block(ControlBlock),
334 | Node(N),
335 | }
336 |
337 | impl Parse for MatchNodeArmBody {
338 | fn parse(input: ParseStream) -> syn::Result {
339 | if input.peek(Brace) {
340 | input.parse().map(Self::Block)
341 | } else {
342 | input.parse().map(Self::Node)
343 | }
344 | }
345 | }
346 |
--------------------------------------------------------------------------------
/hypertext/src/alloc/impls.rs:
--------------------------------------------------------------------------------
1 | use core::fmt::{self, Write};
2 |
3 | use super::alloc::{
4 | borrow::{Cow, ToOwned},
5 | boxed::Box,
6 | rc::Rc,
7 | string::String,
8 | sync::Arc,
9 | vec::Vec,
10 | };
11 | use crate::{
12 | AttributeBuffer, Buffer, Raw, Renderable, Rendered,
13 | context::{AttributeValue, Context, Node},
14 | };
15 |
16 | impl, C: Context> Renderable for Raw {
17 | #[inline]
18 | fn render_to(&self, buffer: &mut Buffer) {
19 | // XSS SAFETY: `Raw` values are expected to be pre-escaped for
20 | // their respective rendering context.
21 | buffer.dangerously_get_string().push_str(self.as_str());
22 | }
23 |
24 | #[inline]
25 | fn render(&self) -> Rendered {
26 | Rendered(self.as_str().into())
27 | }
28 | }
29 |
30 | impl Renderable for fmt::Arguments<'_> {
31 | #[inline]
32 | fn render_to(&self, buffer: &mut Buffer) {
33 | struct ElementEscaper<'a>(&'a mut String);
34 |
35 | impl Write for ElementEscaper<'_> {
36 | #[inline]
37 | fn write_str(&mut self, s: &str) -> fmt::Result {
38 | html_escape::encode_text_to_string(s, self.0);
39 | Ok(())
40 | }
41 | }
42 |
43 | // XSS SAFETY: `ElementEscaper` will escape special characters.
44 | _ = ElementEscaper(buffer.dangerously_get_string()).write_fmt(*self);
45 | }
46 | }
47 |
48 | impl Renderable for fmt::Arguments<'_> {
49 | #[inline]
50 | fn render_to(&self, buffer: &mut AttributeBuffer) {
51 | struct AttributeEscaper<'a>(&'a mut String);
52 |
53 | impl Write for AttributeEscaper<'_> {
54 | #[inline]
55 | fn write_str(&mut self, s: &str) -> fmt::Result {
56 | html_escape::encode_double_quoted_attribute_to_string(s, self.0);
57 | Ok(())
58 | }
59 | }
60 |
61 | // XSS SAFETY: `AttributeEscaper` will escape special characters.
62 | _ = AttributeEscaper(buffer.dangerously_get_string()).write_fmt(*self);
63 | }
64 | }
65 |
66 | impl Renderable for char {
67 | #[inline]
68 | fn render_to(&self, buffer: &mut Buffer) {
69 | let s = buffer.dangerously_get_string();
70 | match *self {
71 | '&' => s.push_str("&"),
72 | '<' => s.push_str("<"),
73 | '>' => s.push_str(">"),
74 | c => s.push(c),
75 | }
76 | }
77 |
78 | #[inline]
79 | fn render(&self) -> Rendered {
80 | Rendered(match *self {
81 | '&' => "&".into(),
82 | '<' => "<".into(),
83 | '>' => ">".into(),
84 | c => c.into(),
85 | })
86 | }
87 | }
88 |
89 | impl Renderable for char {
90 | #[inline]
91 | fn render_to(&self, buffer: &mut AttributeBuffer) {
92 | // XSS SAFETY: we are manually performing escaping here
93 | let s = buffer.dangerously_get_string();
94 |
95 | match *self {
96 | '&' => s.push_str("&"),
97 | '<' => s.push_str("<"),
98 | '>' => s.push_str(">"),
99 | '"' => s.push_str("""),
100 | c => s.push(c),
101 | }
102 | }
103 | }
104 |
105 | impl Renderable for str {
106 | #[inline]
107 | fn render_to(&self, buffer: &mut Buffer) {
108 | // XSS SAFETY: we use `html_escape` to ensure the text is properly escaped
109 | html_escape::encode_text_to_string(self, buffer.dangerously_get_string());
110 | }
111 |
112 | #[inline]
113 | fn render(&self) -> Rendered {
114 | Rendered(html_escape::encode_text(self).into_owned())
115 | }
116 | }
117 |
118 | impl Renderable for str {
119 | #[inline]
120 | fn render_to(&self, buffer: &mut AttributeBuffer) {
121 | // XSS SAFETY: we use `html_escape` to ensure the text is properly escaped
122 | html_escape::encode_double_quoted_attribute_to_string(
123 | self,
124 | buffer.dangerously_get_string(),
125 | );
126 | }
127 | }
128 |
129 | impl Renderable for String {
130 | #[inline]
131 | fn render_to(&self, buffer: &mut Buffer) {
132 | self.as_str().render_to(buffer);
133 | }
134 |
135 | #[inline]
136 | fn render(&self) -> Rendered {
137 | Renderable::::render(self.as_str())
138 | }
139 | }
140 |
141 | impl Renderable for String {
142 | #[inline]
143 | fn render_to(&self, buffer: &mut AttributeBuffer) {
144 | self.as_str().render_to(buffer);
145 | }
146 | }
147 |
148 | impl Renderable for bool {
149 | #[inline]
150 | fn render_to(&self, buffer: &mut Buffer) {
151 | // XSS SAFETY: "true" and "false" are safe strings
152 | buffer
153 | .dangerously_get_string()
154 | .push_str(if *self { "true" } else { "false" });
155 | }
156 |
157 | #[inline]
158 | fn render(&self) -> Rendered {
159 | Rendered(if *self { "true" } else { "false" }.into())
160 | }
161 | }
162 |
163 | macro_rules! render_via_itoa {
164 | ($($Ty:ty)*) => {
165 | $(
166 | impl Renderable for $Ty {
167 | #[inline]
168 | fn render_to(&self, buffer: &mut Buffer) {
169 | // XSS SAFETY: integers are safe
170 | buffer.dangerously_get_string().push_str(itoa::Buffer::new().format(*self));
171 | }
172 |
173 | #[inline]
174 | fn render(&self) -> Rendered {
175 | Rendered(itoa::Buffer::new().format(*self).into())
176 | }
177 | }
178 | )*
179 | };
180 | }
181 |
182 | render_via_itoa! {
183 | i8 i16 i32 i64 i128 isize
184 | u8 u16 u32 u64 u128 usize
185 | }
186 |
187 | macro_rules! render_via_ryu {
188 | ($($Ty:ty)*) => {
189 | $(
190 | impl Renderable for $Ty {
191 | #[inline]
192 | fn render_to(&self, buffer: &mut Buffer) {
193 | // XSS SAFETY: floats are safe
194 | buffer.dangerously_get_string().push_str(ryu::Buffer::new().format(*self));
195 | }
196 |
197 | #[inline]
198 | fn render(&self) -> Rendered {
199 | Rendered(ryu::Buffer::new().format(*self).into())
200 | }
201 | }
202 | )*
203 | };
204 | }
205 |
206 | render_via_ryu! {
207 | f32 f64
208 | }
209 |
210 | macro_rules! render_via_deref {
211 | ($($Ty:ty)*) => {
212 | $(
213 | impl Renderable for $Ty {
214 | #[inline]
215 | fn render_to(&self, buffer: &mut Buffer) {
216 | T::render_to(&**self, buffer);
217 | }
218 |
219 | #[inline]
220 | fn render(&self) -> Rendered {
221 | T::render(&**self)
222 | }
223 | }
224 |
225 | impl + ?Sized> Renderable for $Ty {
226 | #[inline]
227 | fn render_to(&self, buffer: &mut AttributeBuffer) {
228 | T::render_to(&**self, buffer);
229 | }
230 | }
231 | )*
232 | };
233 | }
234 |
235 | render_via_deref! {
236 | &T
237 | &mut T
238 | Box
239 | Rc
240 | Arc
241 | }
242 |
243 | impl<'a, B: 'a + Renderable + ToOwned + ?Sized> Renderable for Cow<'a, B> {
244 | #[inline]
245 | fn render_to(&self, buffer: &mut Buffer) {
246 | B::render_to(&**self, buffer);
247 | }
248 |
249 | #[inline]
250 | fn render(&self) -> Rendered {
251 | B::render(&**self)
252 | }
253 | }
254 |
255 | impl<'a, B: 'a + Renderable + ToOwned + ?Sized> Renderable
256 | for Cow<'a, B>
257 | {
258 | #[inline]
259 | fn render_to(&self, buffer: &mut AttributeBuffer) {
260 | B::render_to(&**self, buffer);
261 | }
262 | }
263 |
264 | impl Renderable for [T] {
265 | #[inline]
266 | fn render_to(&self, buffer: &mut Buffer) {
267 | for item in self {
268 | item.render_to(buffer);
269 | }
270 | }
271 | }
272 |
273 | impl Renderable for [T; N] {
274 | #[inline]
275 | fn render_to(&self, buffer: &mut Buffer) {
276 | self.as_slice().render_to(buffer);
277 | }
278 | }
279 |
280 | impl Renderable for Vec {
281 | #[inline]
282 | fn render_to(&self, buffer: &mut Buffer) {
283 | self.as_slice().render_to(buffer);
284 | }
285 | }
286 |
287 | impl, C: Context> Renderable for Option {
288 | #[inline]
289 | fn render_to(&self, buffer: &mut Buffer) {
290 | if let Some(value) = self {
291 | value.render_to(buffer);
292 | }
293 | }
294 | }
295 |
296 | impl, E: Renderable, C: Context> Renderable for Result {
297 | #[inline]
298 | fn render_to(&self, buffer: &mut Buffer) {
299 | match self {
300 | Ok(value) => value.render_to(buffer),
301 | Err(err) => err.render_to(buffer),
302 | }
303 | }
304 | }
305 |
306 | macro_rules! impl_tuple {
307 | () => {
308 | impl Renderable for () {
309 | #[inline]
310 | fn render_to(&self, _: &mut Buffer) {}
311 | }
312 | };
313 | (($i:tt $T:ident)) => {
314 | #[cfg_attr(docsrs, doc(fake_variadic))]
315 | #[cfg_attr(docsrs, doc = "This trait is implemented for tuples up to twelve items long.")]
316 | impl<$T: Renderable, C: Context> Renderable for ($T,) {
317 | #[inline]
318 | fn render_to(&self, buffer: &mut Buffer) {
319 | self.$i.render_to(buffer);
320 | }
321 | }
322 | };
323 | (($i0:tt $T0:ident) $(($i:tt $T:ident))+) => {
324 | #[cfg_attr(docsrs, doc(hidden))]
325 | impl<$T0: Renderable, $($T: Renderable),*, C: Context> Renderable for ($T0, $($T,)*) {
326 | #[inline]
327 | fn render_to(&self, buffer: &mut Buffer) {
328 | self.$i0.render_to(buffer);
329 | $(self.$i.render_to(buffer);)*
330 | }
331 | }
332 | }
333 | }
334 |
335 | impl_tuple!();
336 | impl_tuple!((0 T));
337 | impl_tuple!((0 T0) (1 T1));
338 | impl_tuple!((0 T0) (1 T1) (2 T2));
339 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3));
340 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4));
341 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5));
342 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6));
343 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7));
344 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8));
345 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8) (9 T9));
346 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8) (9 T9) (10 T10));
347 | impl_tuple!((0 T0) (1 T1) (2 T2) (3 T3) (4 T4) (5 T5) (6 T6) (7 T7) (8 T8) (9 T9) (10 T10) (11 T11));
348 |
--------------------------------------------------------------------------------
/hypertext-macros/src/html/basics.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{self, Display, Formatter, Write};
2 |
3 | use proc_macro2::{Span, TokenStream};
4 | use quote::ToTokens;
5 | use syn::{
6 | Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token,
7 | ext::IdentExt,
8 | parse::{Parse, ParseStream},
9 | spanned::Spanned,
10 | };
11 |
12 | #[derive(PartialEq, Eq, Clone)]
13 | pub struct UnquotedName(Vec);
14 |
15 | impl UnquotedName {
16 | pub fn ident_string(&self) -> String {
17 | let mut s = String::new();
18 |
19 | for fragment in &self.0 {
20 | match fragment {
21 | NameFragment::Ident(ident) => {
22 | _ = write!(s, "{ident}");
23 | }
24 | NameFragment::Int(num) => {
25 | _ = write!(s, "{num}");
26 | }
27 | NameFragment::Hyphen(_) => {
28 | s.push('_');
29 | }
30 | NameFragment::Colon(_) | NameFragment::Dot(_) => {
31 | unreachable!(
32 | "unquoted name idents should only contain identifiers, int literals, and hyphens"
33 | );
34 | }
35 | }
36 | }
37 |
38 | if s == "super"
39 | || s == "self"
40 | || s == "Self"
41 | || s == "extern"
42 | || s == "crate"
43 | || s == "_"
44 | || s.chars().next().is_some_and(|c| c.is_ascii_digit())
45 | {
46 | s.insert(0, '_');
47 | }
48 |
49 | s
50 | }
51 |
52 | pub fn is_component(&self) -> bool {
53 | matches!(
54 | self.0.as_slice(),
55 | [NameFragment::Ident(ident)]
56 | if ident.to_string().chars().next().is_some_and(|c| c.is_ascii_uppercase())
57 | )
58 | }
59 |
60 | pub fn spans(&self) -> Vec {
61 | let mut spans = Vec::new();
62 |
63 | for fragment in &self.0 {
64 | spans.push(fragment.span());
65 | }
66 |
67 | spans
68 | }
69 |
70 | pub fn lits(&self) -> Vec {
71 | let mut strs = Vec::new();
72 |
73 | for fragment in &self.0 {
74 | strs.push(LitStr::new(&fragment.to_string(), fragment.span()));
75 | }
76 |
77 | strs
78 | }
79 |
80 | pub fn parse_any(input: ParseStream) -> syn::Result {
81 | let mut name = Vec::new();
82 |
83 | while input.peek(Token![-])
84 | || input.peek(Token![:])
85 | || input.peek(Token![.])
86 | || (name.last().is_none_or(NameFragment::is_punct)
87 | && (input.peek(Ident::peek_any) || input.peek(LitInt)))
88 | {
89 | name.push(input.parse()?);
90 | }
91 |
92 | Ok(Self(name))
93 | }
94 |
95 | pub fn parse_attr_value(input: ParseStream) -> syn::Result {
96 | let lookahead = input.lookahead1();
97 |
98 | let mut name = Vec::new();
99 |
100 | if lookahead.peek(Ident::peek_any) || lookahead.peek(LitInt) {
101 | name.push(input.parse()?);
102 |
103 | while input.peek(Token![-])
104 | || input.peek(Token![:])
105 | || (name.last().is_none_or(NameFragment::is_punct)
106 | && (input.peek(Ident::peek_any) || input.peek(LitInt)))
107 | {
108 | name.push(input.parse()?);
109 | }
110 |
111 | Ok(Self(name))
112 | } else {
113 | Err(lookahead.error())
114 | }
115 | }
116 | }
117 |
118 | impl Parse for UnquotedName {
119 | fn parse(input: ParseStream) -> syn::Result {
120 | let lookahead = input.lookahead1();
121 |
122 | let mut name = Vec::new();
123 |
124 | if lookahead.peek(Ident::peek_any) || lookahead.peek(LitInt) {
125 | name.push(input.parse()?);
126 |
127 | while input.peek(Token![-])
128 | || (name.last().is_none_or(NameFragment::is_punct)
129 | && (input.peek(Ident::peek_any) || input.peek(LitInt)))
130 | {
131 | name.push(input.parse()?);
132 | }
133 |
134 | Ok(Self(name))
135 | } else {
136 | Err(lookahead.error())
137 | }
138 | }
139 | }
140 |
141 | #[derive(Clone, PartialEq, Eq)]
142 | enum NameFragment {
143 | Ident(Ident),
144 | Int(LitInt),
145 | Hyphen(Token![-]),
146 | Colon(Token![:]),
147 | Dot(Token![.]),
148 | }
149 |
150 | impl NameFragment {
151 | fn span(&self) -> Span {
152 | match self {
153 | Self::Ident(ident) => ident.span(),
154 | Self::Int(int) => int.span(),
155 | Self::Hyphen(hyphen) => hyphen.span(),
156 | Self::Colon(colon) => colon.span(),
157 | Self::Dot(dot) => dot.span(),
158 | }
159 | }
160 |
161 | const fn is_punct(&self) -> bool {
162 | matches!(self, Self::Hyphen(_) | Self::Colon(_) | Self::Dot(_))
163 | }
164 | }
165 |
166 | impl Parse for NameFragment {
167 | fn parse(input: ParseStream) -> syn::Result {
168 | let lookahead = input.lookahead1();
169 |
170 | if lookahead.peek(Token![-]) {
171 | input.parse().map(Self::Hyphen)
172 | } else if lookahead.peek(Token![:]) {
173 | input.parse().map(Self::Colon)
174 | } else if lookahead.peek(Token![.]) {
175 | input.parse().map(Self::Dot)
176 | } else if lookahead.peek(Ident::peek_any) {
177 | input.call(Ident::parse_any).map(Self::Ident)
178 | } else if lookahead.peek(LitInt) {
179 | input.parse().map(Self::Int)
180 | } else {
181 | Err(lookahead.error())
182 | }
183 | }
184 | }
185 |
186 | impl Display for NameFragment {
187 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
188 | match self {
189 | Self::Ident(ident) => write!(f, "{ident}"),
190 | Self::Int(num) => write!(f, "{num}"),
191 | Self::Hyphen(_) => f.write_str("-"),
192 | Self::Colon(_) => f.write_str(":"),
193 | Self::Dot(_) => f.write_str("."),
194 | }
195 | }
196 | }
197 |
198 | pub enum Literal {
199 | Str(LitStr),
200 | Int(LitInt),
201 | Bool(LitBool),
202 | Float(LitFloat),
203 | Char(LitChar),
204 | }
205 |
206 | impl Literal {
207 | pub fn lit_str(&self) -> LitStr {
208 | match self {
209 | Self::Str(lit) => lit.clone(),
210 | Self::Int(lit) => LitStr::new(&lit.to_string(), lit.span()),
211 | Self::Bool(lit) => LitStr::new(&lit.value.to_string(), lit.span()),
212 | Self::Float(lit) => LitStr::new(&lit.to_string(), lit.span()),
213 | Self::Char(lit) => LitStr::new(&lit.value().to_string(), lit.span()),
214 | }
215 | }
216 |
217 | pub fn parse_any(input: ParseStream) -> syn::Result {
218 | let lookahead = input.lookahead1();
219 |
220 | if lookahead.peek(LitStr) {
221 | input.parse().map(Self::Str)
222 | } else if lookahead.peek(LitInt) {
223 | input.parse().map(Self::Int)
224 | } else if lookahead.peek(LitBool) {
225 | input.parse().map(Self::Bool)
226 | } else if lookahead.peek(LitFloat) {
227 | input.parse().map(Self::Float)
228 | } else if lookahead.peek(LitChar) {
229 | input.parse().map(Self::Char)
230 | } else {
231 | Err(lookahead.error())
232 | }
233 | }
234 | }
235 |
236 | impl Parse for Literal {
237 | fn parse(input: ParseStream) -> syn::Result {
238 | let lookahead = input.lookahead1();
239 |
240 | if lookahead.peek(LitStr) {
241 | let lit = input.parse::()?;
242 | if !lit.suffix().is_empty() {
243 | let suffix = lit.suffix();
244 | let next_quote = if input.peek(LitStr) { r#"\""# } else { "" };
245 | return Err(syn::Error::new_spanned(
246 | &lit,
247 | format!(
248 | r#"string suffixes are not allowed in literals (you probably meant `"...\"{suffix}{next_quote}..."` or `"..." {suffix}`)"#,
249 | ),
250 | ));
251 | }
252 | let value = unindent(&lit.value());
253 | Ok(Self::Str(LitStr::new(&value, lit.span())))
254 | } else if lookahead.peek(LitInt) {
255 | let lit = input.parse::()?;
256 | if !lit.suffix().is_empty() {
257 | return Err(syn::Error::new_spanned(
258 | &lit,
259 | "integer literals cannot have suffixes",
260 | ));
261 | }
262 | Ok(Self::Int(lit))
263 | } else if lookahead.peek(LitBool) {
264 | input.parse().map(Self::Bool)
265 | } else if lookahead.peek(LitFloat) {
266 | let lit = input.parse::