Added a few minky magic components
This commit is contained in:
parent
4939d5a58f
commit
54e47e68f7
15 changed files with 801 additions and 531 deletions
98
Cargo.lock
generated
98
Cargo.lock
generated
|
@ -260,6 +260,8 @@ dependencies = [
|
|||
"eyre",
|
||||
"git2",
|
||||
"icondata",
|
||||
"magic",
|
||||
"tailwind_fuse",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
|
@ -268,6 +270,41 @@ dependencies = [
|
|||
"vespid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.20.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive-where"
|
||||
version = "1.2.7"
|
||||
|
@ -654,6 +691,12 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
|
@ -780,6 +823,16 @@ version = "0.4.22"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "magic"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"icondata",
|
||||
"icondata_core",
|
||||
"tailwind_fuse",
|
||||
"vespid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
|
@ -817,6 +870,12 @@ dependencies = [
|
|||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.7.4"
|
||||
|
@ -846,6 +905,16 @@ dependencies = [
|
|||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
|
@ -1225,6 +1294,12 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.90"
|
||||
|
@ -1271,6 +1346,28 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tailwind_fuse"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e9d32d52c3191836fe1858b6b38442d1e536eeb11883b2041e6db080a208c2d"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"tailwind_fuse_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tailwind_fuse_macro"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89fd8a13e8e105a886fe9d15aa60580602be9ee9a17235e552f19faa3d7834f4"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
@ -1606,7 +1703,6 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"axum",
|
||||
"html-escape",
|
||||
"icondata_core",
|
||||
"serde",
|
||||
"thiserror 2.0.6",
|
||||
"tokio",
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -1,12 +1,15 @@
|
|||
[workspace]
|
||||
members = [
|
||||
".",
|
||||
".", "magic",
|
||||
"vespid",
|
||||
"vespid/macros"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
vespid.path = "vespid"
|
||||
magic.path = "magic"
|
||||
icondata = { version = "0.5", default-features = false, features = ["lucide"] }
|
||||
tailwind_fuse = { version = "0.3.1", features = ["variant"] }
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
|
@ -16,7 +19,8 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
vespid = { workspace = true, features = ["icons"] }
|
||||
vespid.workspace = true
|
||||
magic.workspace = true
|
||||
tokio.workspace = true
|
||||
axum.workspace = true
|
||||
eyre = "0.6.12"
|
||||
|
@ -25,5 +29,6 @@ tracing = "0.1.41"
|
|||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tracing-error = "0.2.1"
|
||||
git2 = "0.19.0"
|
||||
icondata = { version = "0.5", default-features = false, features = ["lucide"] }
|
||||
icondata.workspace = true
|
||||
tower-http = { version = "0.6.2", features = ["full"] }
|
||||
tailwind_fuse.workspace = true
|
||||
|
|
10
magic/Cargo.toml
Normal file
10
magic/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "magic"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
icondata_core = "0.1"
|
||||
icondata.workspace = true
|
||||
vespid.workspace = true
|
||||
tailwind_fuse.workspace = true
|
65
magic/src/button.rs
Normal file
65
magic/src/button.rs
Normal file
|
@ -0,0 +1,65 @@
|
|||
use tailwind_fuse::*;
|
||||
|
||||
#[derive(TwClass)]
|
||||
#[tw(
|
||||
class = "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50"
|
||||
)]
|
||||
pub struct Button {
|
||||
pub variant: ButtonVariant,
|
||||
pub size: ButtonSize,
|
||||
}
|
||||
|
||||
#[derive(TwVariant)]
|
||||
pub enum ButtonVariant {
|
||||
#[tw(
|
||||
default,
|
||||
class = "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
)]
|
||||
Primary,
|
||||
#[tw(
|
||||
class = "border-primary border bg-transparent text-foreground hover:bg-primary hover:text-primary-foreground"
|
||||
)]
|
||||
OutlinePrimary,
|
||||
#[tw(class = "bg-secondary text-secondary-foreground hover:bg-secondary/90")]
|
||||
Secondary,
|
||||
#[tw(class = "bg-success text-success-foreground hover:bg-success/90")]
|
||||
Success,
|
||||
#[tw(
|
||||
class = "border-success border bg-transparent text-foreground hover:bg-success hover:text-success-foreground"
|
||||
)]
|
||||
OutlineSuccess,
|
||||
#[tw(class = "bg-destructive text-destructive-foreground hover:bg-destructive/90")]
|
||||
Destructive,
|
||||
#[tw(
|
||||
class = "border-destructive border bg-transparent text-foreground hover:bg-destructive hover:text-destructive-foreground"
|
||||
)]
|
||||
OutlineDestructive,
|
||||
#[tw(
|
||||
class = "border border-input bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground"
|
||||
)]
|
||||
Outline,
|
||||
#[tw(
|
||||
class = "border border-input bg-transparent text-foreground hover:bg-primary hover:text-primary-foreground flex items-center gap-1"
|
||||
)]
|
||||
OutlineIcon,
|
||||
#[tw(class = "hover:bg-accent hover:text-accent-foreground")]
|
||||
Ghost,
|
||||
#[tw(class = "bg-secondary/80 text-accent-foreground hover:text-accent-foreground")]
|
||||
Link,
|
||||
#[tw(
|
||||
class = "bg-secondary/80 text-accent-foreground hover:text-accent-foreground flex items-center gap-1"
|
||||
)]
|
||||
Icon,
|
||||
}
|
||||
|
||||
#[derive(TwVariant)]
|
||||
pub enum ButtonSize {
|
||||
#[tw(default, class = "h-10 px-4 py-2")]
|
||||
Default,
|
||||
#[tw(class = "h-9 rounded-md px-3")]
|
||||
Small,
|
||||
#[tw(class = "h-11 rounded-md px-8")]
|
||||
Large,
|
||||
#[tw(class = "h-10 w-10")]
|
||||
Icon,
|
||||
}
|
64
magic/src/card.rs
Normal file
64
magic/src/card.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use tailwind_fuse::tw_merge;
|
||||
use vespid::{MaybeText, component, view};
|
||||
|
||||
pub const CARD: &str = "rounded-lg border bg-card text-card-foreground shadow-sm";
|
||||
pub const CARD_HEADER: &str = "flex flex-col gap-2 p-6";
|
||||
pub const CARD_TITLE: &str = "text-lg font-semibold leading-none tracking-tight";
|
||||
pub const CARD_DESCRIPTION: &str = "text-sm text-muted-foreground";
|
||||
pub const CARD_CONTENT: &str = "p-6 pt-0";
|
||||
pub const CARD_FOOTER: &str = "flex items-center p-6 pt-0";
|
||||
|
||||
#[component]
|
||||
pub async fn Card(
|
||||
#[builder(default, setter(into))] class: MaybeText,
|
||||
#[builder(default, setter(into))] id: MaybeText,
|
||||
children: String,
|
||||
) -> String {
|
||||
let class = tw_merge!(CARD, class.get().unwrap_or(""));
|
||||
view! { <div class={class} id=id.get()>{children}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub async fn CardHeader(
|
||||
#[builder(default, setter(into))] class: MaybeText,
|
||||
children: String,
|
||||
) -> String {
|
||||
let class = tw_merge!(CARD_HEADER, class.get().unwrap_or(""));
|
||||
view! { <div class={class}>{children}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub async fn CardTitle(
|
||||
#[builder(default, setter(into))] class: MaybeText,
|
||||
children: String,
|
||||
) -> String {
|
||||
let class = tw_merge!(CARD_TITLE, class.get().unwrap_or(""));
|
||||
view! { <div class={class}>{children}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub async fn CardDescription(
|
||||
#[builder(default, setter(into))] class: MaybeText,
|
||||
children: String,
|
||||
) -> String {
|
||||
let class = tw_merge!(CARD_DESCRIPTION, class.get().unwrap_or(""));
|
||||
view! { <div class={class}>{children}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub async fn CardContent(
|
||||
#[builder(default, setter(into))] class: MaybeText,
|
||||
children: String,
|
||||
) -> String {
|
||||
let class = tw_merge!(CARD_CONTENT, class.get().unwrap_or(""));
|
||||
view! { <div class={class}>{children}</div> }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub async fn CardFooter(
|
||||
#[builder(default, setter(into))] class: MaybeText,
|
||||
children: String,
|
||||
) -> String {
|
||||
let class = tw_merge!(CARD_FOOTER, class.get().unwrap_or(""));
|
||||
view! { <div class={class}>{children}</div> }
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
use super::*;
|
||||
use vespid::prelude::*;
|
||||
pub use icondata_core::Icon;
|
||||
|
||||
#[vespid_macros::component]
|
||||
#[component]
|
||||
pub fn Icon(
|
||||
icon: Icon,
|
||||
#[builder(default, setter(into))] width: MaybeText,
|
13
magic/src/lib.rs
Normal file
13
magic/src/lib.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#![allow(non_snake_case)]
|
||||
|
||||
pub mod button;
|
||||
pub mod card;
|
||||
pub mod icons;
|
||||
pub mod spinner;
|
||||
|
||||
pub mod prelude {
|
||||
pub use tailwind_fuse::{IntoBuilder, IntoTailwindClass, tw_merge};
|
||||
pub use vespid::prelude::*;
|
||||
|
||||
pub use crate::{button::*, card::*, icons::*, spinner::*};
|
||||
}
|
12
magic/src/spinner.rs
Normal file
12
magic/src/spinner.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use tailwind_fuse::tw_merge;
|
||||
use vespid::{MaybeText, component, view};
|
||||
|
||||
#[component]
|
||||
pub async fn Spinner(#[builder(default, setter(into))] class: MaybeText) -> String {
|
||||
let class = tw_merge!(
|
||||
"htmx-indicator animate-spin spinner h-12 w-12",
|
||||
class.get().unwrap_or("")
|
||||
);
|
||||
|
||||
view! { <crate::icons::Icon icon=icondata::LuLoader2 class=class /> }
|
||||
}
|
25
src/main.rs
25
src/main.rs
|
@ -8,7 +8,8 @@ use std::sync::{atomic::AtomicU64, Arc};
|
|||
use axum::{response::Html, routing::get};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use vespid::{axum::render, prelude::*};
|
||||
use magic::prelude::*;
|
||||
use vespid::axum::render;
|
||||
|
||||
#[component]
|
||||
fn Shell(children: String) -> String {
|
||||
|
@ -22,7 +23,7 @@ fn Shell(children: String) -> String {
|
|||
<script src="https://unpkg.com/htmx.org@2.0.3" integrity="sha384-0895/pl2MU10Hqc6jd4RvrthNlDiE9U1tWmX7WRESftEDRosgxNsQG/Ze9YMRzHq" crossorigin="anonymous"></script>
|
||||
<style>{include_str!("../target/app.css")}</style>
|
||||
</head>
|
||||
<body>
|
||||
<body class="flex flex-col">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -36,12 +37,8 @@ async fn index() -> Html<String> {
|
|||
<h1>"Hello to Crusto!"</h1>
|
||||
<p>"Index"</p>
|
||||
|
||||
<button hx-get="/widget" hx-swap="outerHTML" hx-target="#widget" hx-trigger="load delay:1s, click" hx-indicator="#widget_container > .spinner">"Get widget"</button>
|
||||
|
||||
<div id="widget_container" class="flex flex-col items-center justify-center">
|
||||
<Icon icon=icondata::LuLoader2 class="animate-spin spinner htmx-indicator" />
|
||||
<div id="widget">
|
||||
</div>
|
||||
<div id="widget" class="flex flex-col items-center justify-center" hx-get="/widget" hx-swap="outerHTML" hx-trigger="load" hx-indicator="#widget > .spinner">
|
||||
<Spinner />
|
||||
</div>
|
||||
</Shell>
|
||||
}
|
||||
|
@ -68,10 +65,14 @@ async fn main() -> eyre::Result<()> {
|
|||
let app = axum::Router::new().route("/", get(index)).route("/widget", get(|| async move {
|
||||
render(async move {
|
||||
view! {
|
||||
<div style="background-color: red; color: white; padding: 10px; border-radius: 5px;" id="widget">
|
||||
<h2>"Widget"</h2>
|
||||
<p>{amount_of_refreshes.fetch_add(1, std::sync::atomic::Ordering::Relaxed)}</p>
|
||||
</div>
|
||||
<Card id="widget" class="w-64 flex flex-col">
|
||||
<CardHeader>
|
||||
<CardTitle>"Widget"</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>{amount_of_refreshes.fetch_add(1, std::sync::atomic::Ordering::Relaxed)} " refreshes"</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
}).await
|
||||
}));
|
||||
|
|
|
@ -10,10 +10,5 @@ tokio-util = { version = "0.7.13", features = ["rt"] }
|
|||
axum.workspace = true
|
||||
vespid_macros.path = "macros"
|
||||
typed-builder = "0.20.0"
|
||||
|
||||
icondata_core = { version = "0.1", optional = true }
|
||||
serde = "1.0.215"
|
||||
thiserror = "2.0.6"
|
||||
|
||||
[features]
|
||||
icons = ["dep:icondata_core"]
|
||||
|
|
163
vespid/macros/src/component.rs
Normal file
163
vespid/macros/src/component.rs
Normal file
|
@ -0,0 +1,163 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro_error2::{abort, OptionExt};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{parse::Parse, parse_quote, punctuated::Punctuated, FnArg, ItemStruct, Token};
|
||||
|
||||
struct PropsStruct {
|
||||
name: syn::Ident,
|
||||
item: ItemStruct,
|
||||
}
|
||||
|
||||
impl Parse for PropsStruct {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let item = input.parse::<ItemStruct>()?;
|
||||
let name = item.ident.clone();
|
||||
|
||||
Ok(PropsStruct { name, item })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PropsStruct {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let name = &self.name;
|
||||
let item = &self.item;
|
||||
|
||||
let builder_name =
|
||||
syn::Ident::new(&format!("{}Builder", name), proc_macro2::Span::call_site());
|
||||
|
||||
tokens.extend(quote! {
|
||||
#[derive(::vespid::typed_builder::TypedBuilder)]
|
||||
#[builder(doc, crate_module_path=::vespid::typed_builder)]
|
||||
#item
|
||||
|
||||
impl ::vespid::support::Props for #name {
|
||||
type Builder = #builder_name;
|
||||
fn builder() -> Self::Builder {
|
||||
#name::builder()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let has_attributes = item.fields.iter().any(|field| {
|
||||
field
|
||||
.ident
|
||||
.as_ref()
|
||||
.expect_or_abort("field.ident == None")
|
||||
.to_string()
|
||||
== "attributes"
|
||||
});
|
||||
|
||||
if has_attributes {
|
||||
tokens.extend(quote! {
|
||||
impl #builder_name {
|
||||
pub fn push_attr<A: std::fmt::Display>(mut self, attr: A) -> Self {
|
||||
self.props.attributes.push_str(&format!("{} ", attr));
|
||||
self
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn props(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
|
||||
let props = syn::parse2::<PropsStruct>(input).unwrap();
|
||||
quote! { #props }.to_token_stream()
|
||||
}
|
||||
|
||||
pub fn process(input: TokenStream) -> TokenStream {
|
||||
let comp = syn::parse_macro_input!(input as ComponentFn);
|
||||
quote! { #comp }.to_token_stream().into()
|
||||
}
|
||||
|
||||
struct ComponentFn {
|
||||
item: syn::ItemFn,
|
||||
}
|
||||
|
||||
impl Parse for ComponentFn {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let item = input.parse::<syn::ItemFn>()?;
|
||||
Ok(ComponentFn { item })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ComponentFn {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let item = &self.item;
|
||||
let name = &item.sig.ident;
|
||||
|
||||
let (defs, args) = match item.sig.inputs.len() {
|
||||
0 => {
|
||||
// generate empty props
|
||||
let props_name =
|
||||
syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
|
||||
(
|
||||
props(quote! {
|
||||
pub struct #props_name{}
|
||||
}),
|
||||
quote! { _props: #props_name },
|
||||
)
|
||||
}
|
||||
// match if there is a single arg of type #nameProps
|
||||
1 if matches!(item.sig.inputs.first().unwrap(), syn::FnArg::Typed(arg) if matches!(arg.ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string() == format!("{}Props", name))) =>
|
||||
{
|
||||
let props = item.sig.inputs.first().unwrap();
|
||||
(quote! {}, props.to_token_stream())
|
||||
}
|
||||
_ => {
|
||||
let field_defs = &item
|
||||
.sig
|
||||
.inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|i| match i {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(i, "builders do not support self");
|
||||
}
|
||||
FnArg::Typed(mut t) => {
|
||||
if t.attrs.is_empty() {
|
||||
t.attrs.push(parse_quote! { #[builder(setter(into))] });
|
||||
}
|
||||
|
||||
t
|
||||
}
|
||||
})
|
||||
.collect::<Punctuated<_, Token![,]>>();
|
||||
let field_names = item
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|i| match i {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(i, "builders do not support self");
|
||||
}
|
||||
FnArg::Typed(t) => &t.pat,
|
||||
})
|
||||
.collect::<Punctuated<_, Token![,]>>();
|
||||
let props_name =
|
||||
syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
|
||||
|
||||
(
|
||||
props(quote! {
|
||||
pub struct #props_name {
|
||||
#field_defs
|
||||
}
|
||||
}),
|
||||
quote! { #props_name { #field_names }: #props_name },
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let body = &item.block;
|
||||
let output = &item.sig.output;
|
||||
let vis = &item.vis;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#defs
|
||||
#[allow(non_snake_case)]
|
||||
#vis async fn #name(#args) #output {
|
||||
#body
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,514 +1,19 @@
|
|||
use std::collections::HashSet;
|
||||
mod component;
|
||||
mod view;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2_diagnostics::Diagnostic;
|
||||
use proc_macro_error2::{abort, OptionExt};
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use rstml::{
|
||||
node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName},
|
||||
Infallible,
|
||||
Parser,
|
||||
ParserConfig,
|
||||
};
|
||||
use syn::{
|
||||
parse::Parse,
|
||||
parse_quote,
|
||||
punctuated::Punctuated,
|
||||
spanned::Spanned,
|
||||
Expr,
|
||||
ExprLit,
|
||||
FnArg,
|
||||
ItemStruct,
|
||||
Token,
|
||||
};
|
||||
|
||||
#[proc_macro]
|
||||
pub fn view(tokens: TokenStream) -> TokenStream {
|
||||
html_inner(tokens, false)
|
||||
view::process(tokens, false)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn view_docs(tokens: TokenStream) -> TokenStream {
|
||||
html_inner(tokens, true)
|
||||
}
|
||||
|
||||
fn is_empty_element(name: &str) -> bool {
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
match name {
|
||||
"img" | "input" | "meta" | "link" | "hr" | "br" | "source" | "track" | "wbr" | "area"
|
||||
| "base" | "col" | "embed" | "param" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_elements_set() -> HashSet<&'static str> {
|
||||
[
|
||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
|
||||
"source", "track", "wbr",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn html_inner(tokens: TokenStream, ide_helper: bool) -> TokenStream {
|
||||
let config = ParserConfig::new()
|
||||
.recover_block(true)
|
||||
.element_close_use_default_wildcard_ident(true)
|
||||
.always_self_closed_elements(empty_elements_set());
|
||||
|
||||
let parser = Parser::new(config);
|
||||
let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
|
||||
process_nodes(ide_helper, &nodes, errors).into()
|
||||
}
|
||||
|
||||
fn process_nodes<'n>(
|
||||
ide_helper: bool,
|
||||
nodes: &'n Vec<Node>,
|
||||
errors: Vec<Diagnostic>,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let WalkNodesOutput {
|
||||
static_format: html_string,
|
||||
values,
|
||||
collected_elements: elements,
|
||||
diagnostics,
|
||||
} = walk_nodes(&nodes);
|
||||
let docs = if ide_helper {
|
||||
generate_tags_docs(elements)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let errors = errors
|
||||
.into_iter()
|
||||
.map(|e| e.emit_as_expr_tokens())
|
||||
.chain(diagnostics);
|
||||
quote! {
|
||||
{
|
||||
// Make sure that "compile_error!(..);" can be used in this context.
|
||||
#(#errors;)*
|
||||
// Make sure that "enum x{};" and "let _x = crate::element;" can be used in this context
|
||||
#(#docs;)*
|
||||
format!(#html_string, #(vespid::FormatRender::new(#values)),*)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec<proc_macro2::TokenStream> {
|
||||
// Mark some of elements as type,
|
||||
// and other as elements as fn in crate::docs,
|
||||
// to give an example how to link tag with docs.
|
||||
let elements_as_type: HashSet<&'static str> =
|
||||
vec!["html", "head", "meta", "link", "body", "div"]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
elements
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
if elements_as_type.contains(&*e.to_string()) {
|
||||
let element = quote_spanned!(e.span() => enum);
|
||||
quote!({#element X{}})
|
||||
} else {
|
||||
// let _ = crate::docs::element;
|
||||
let element = quote_spanned!(e.span() => element);
|
||||
quote!(let _ = crate::docs::#element)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WalkNodesOutput<'a> {
|
||||
static_format: String,
|
||||
// Use proc_macro2::TokenStream instead of syn::Expr
|
||||
// to provide more errors to the end user.
|
||||
values: Vec<proc_macro2::TokenStream>,
|
||||
// Additional diagnostic messages.
|
||||
diagnostics: Vec<proc_macro2::TokenStream>,
|
||||
// Collect elements to provide semantic highlight based on element tag.
|
||||
// No differences between open tag and closed tag.
|
||||
// Also multiple tags with same name can be present,
|
||||
// because we need to mark each of them.
|
||||
collected_elements: Vec<&'a NodeName>,
|
||||
}
|
||||
impl<'a> WalkNodesOutput<'a> {
|
||||
fn extend(&mut self, other: WalkNodesOutput<'a>) {
|
||||
self.static_format.push_str(&other.static_format);
|
||||
self.values.extend(other.values);
|
||||
self.diagnostics.extend(other.diagnostics);
|
||||
self.collected_elements.extend(other.collected_elements);
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_nodes<'a>(nodes: &'a Vec<Node>) -> WalkNodesOutput<'a> {
|
||||
let mut out = WalkNodesOutput::default();
|
||||
|
||||
for node in nodes {
|
||||
match node {
|
||||
Node::Doctype(doctype) => {
|
||||
let value = &doctype.value.to_token_stream_string();
|
||||
out.static_format.push_str(&format!("<!DOCTYPE {}>", value));
|
||||
}
|
||||
Node::Element(element) => {
|
||||
let name = element.name().to_string();
|
||||
|
||||
if !is_component_tag_name(&name) {
|
||||
match element.name() {
|
||||
NodeName::Block(block) => {
|
||||
out.static_format.push_str("<{}");
|
||||
out.values.push(block.to_token_stream());
|
||||
}
|
||||
_ => {
|
||||
out.static_format.push_str(&format!("<{}", name));
|
||||
out.collected_elements.push(&element.open_tag.name);
|
||||
if let Some(e) = &element.close_tag {
|
||||
out.collected_elements.push(&e.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attributes
|
||||
for attribute in element.attributes() {
|
||||
match attribute {
|
||||
NodeAttribute::Block(block) => {
|
||||
// If the nodes parent is an attribute we prefix with whitespace
|
||||
out.static_format.push(' ');
|
||||
out.static_format.push_str("{}");
|
||||
out.values.push(block.to_token_stream());
|
||||
}
|
||||
NodeAttribute::Attribute(attribute) => {
|
||||
let (static_format, value) = walk_attribute(attribute);
|
||||
out.static_format.push_str(&static_format);
|
||||
if let Some(value) = value {
|
||||
out.values.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore childs of special Empty elements
|
||||
if is_empty_element(element.open_tag.name.to_string().as_str()) {
|
||||
out.static_format.push_str(" />");
|
||||
if !element.children.is_empty() {
|
||||
let warning = proc_macro2_diagnostics::Diagnostic::spanned(
|
||||
element.open_tag.name.span(),
|
||||
proc_macro2_diagnostics::Level::Warning,
|
||||
"Element is processed as empty, and cannot have any child",
|
||||
);
|
||||
out.diagnostics.push(warning.emit_as_expr_tokens())
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
out.static_format.push('>');
|
||||
|
||||
// children
|
||||
let other_output = walk_nodes(&element.children);
|
||||
out.extend(other_output);
|
||||
|
||||
match element.name() {
|
||||
NodeName::Block(block) => {
|
||||
out.static_format.push_str("</{}>");
|
||||
out.values.push(block.to_token_stream());
|
||||
}
|
||||
_ => {
|
||||
out.static_format.push_str(&format!("</{}>", name));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// custom elements
|
||||
out.static_format.push_str("{}");
|
||||
out.values
|
||||
.push(CustomElement::new(element).to_token_stream());
|
||||
}
|
||||
}
|
||||
Node::Text(text) => {
|
||||
out.static_format.push_str(&text.value_string());
|
||||
}
|
||||
Node::RawText(text) => {
|
||||
out.static_format.push_str(&text.to_string_best());
|
||||
}
|
||||
Node::Fragment(fragment) => {
|
||||
let other_output = walk_nodes(&fragment.children);
|
||||
out.extend(other_output)
|
||||
}
|
||||
Node::Comment(comment) => {
|
||||
out.static_format.push_str("<!-- {} -->");
|
||||
out.values.push(comment.value.to_token_stream());
|
||||
}
|
||||
Node::Block(block) => {
|
||||
let block = block.try_block().unwrap();
|
||||
let stmts = &block.stmts;
|
||||
out.static_format.push_str("{}");
|
||||
out.values.push(quote!(#(#stmts)*));
|
||||
}
|
||||
Node::Custom(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn walk_attribute(attribute: &KeyedAttribute) -> (String, Option<proc_macro2::TokenStream>) {
|
||||
let mut static_format = String::new();
|
||||
let mut format_value = None;
|
||||
let key = match attribute.key.to_string().as_str() {
|
||||
"as_" => "as".to_string(),
|
||||
_ => attribute.key.to_string(),
|
||||
};
|
||||
static_format.push_str(&format!(" {}", key));
|
||||
|
||||
match attribute.value() {
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Str(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(
|
||||
r#"="{}""#,
|
||||
html_escape::encode_unquoted_attribute(&value.value())
|
||||
));
|
||||
}
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Bool(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(r#"="{}""#, value.value()));
|
||||
}
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Int(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(r#"="{}""#, value.token()));
|
||||
}
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Float(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(r#"="{}""#, value.token()));
|
||||
}
|
||||
Some(value) => {
|
||||
static_format.push_str(r#"="{}""#);
|
||||
format_value = Some(
|
||||
quote! {{
|
||||
::vespid::EscapeAttribute::escape_attribute(&#value)
|
||||
}}
|
||||
.into_token_stream(),
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
(static_format, format_value)
|
||||
}
|
||||
|
||||
fn is_component_tag_name(name: &str) -> bool {
|
||||
name.starts_with(|c: char| c.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
struct CustomElement<'e> {
|
||||
e: &'e NodeElement<Infallible>,
|
||||
}
|
||||
|
||||
impl<'e> CustomElement<'e> {
|
||||
fn new(e: &'e NodeElement<Infallible>) -> Self {
|
||||
CustomElement { e }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e> ToTokens for CustomElement<'_> {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let name = self.e.name();
|
||||
|
||||
let mut chain = vec![quote! {
|
||||
::vespid::support::props_builder(&#name)
|
||||
}];
|
||||
|
||||
let children = &self.e.children;
|
||||
if !children.is_empty() {
|
||||
let c = process_nodes(false, children, vec![]);
|
||||
chain.push(quote! { .children(#c) });
|
||||
}
|
||||
|
||||
chain.push({
|
||||
self.e
|
||||
.attributes()
|
||||
.iter()
|
||||
.map(|a| match a {
|
||||
NodeAttribute::Block(block) => {
|
||||
quote! {
|
||||
.push_attr(
|
||||
#[allow(unused_braces)]
|
||||
#block
|
||||
)
|
||||
}
|
||||
}
|
||||
NodeAttribute::Attribute(attribute) => {
|
||||
let key = &attribute.key;
|
||||
let value = attribute.value().unwrap();
|
||||
quote! { .#key(#value) }
|
||||
}
|
||||
})
|
||||
.collect::<proc_macro2::TokenStream>()
|
||||
});
|
||||
|
||||
chain.push(quote! { .build() });
|
||||
|
||||
tokens.extend(quote! {
|
||||
#name(#(#chain)*).await
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn props(input: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
|
||||
let props = syn::parse2::<PropsStruct>(input).unwrap();
|
||||
quote! { #props }.to_token_stream()
|
||||
}
|
||||
|
||||
struct PropsStruct {
|
||||
name: syn::Ident,
|
||||
item: ItemStruct,
|
||||
}
|
||||
|
||||
impl Parse for PropsStruct {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let item = input.parse::<ItemStruct>()?;
|
||||
let name = item.ident.clone();
|
||||
|
||||
Ok(PropsStruct { name, item })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for PropsStruct {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let name = &self.name;
|
||||
let item = &self.item;
|
||||
|
||||
let builder_name =
|
||||
syn::Ident::new(&format!("{}Builder", name), proc_macro2::Span::call_site());
|
||||
|
||||
tokens.extend(quote! {
|
||||
#[derive(::vespid::typed_builder::TypedBuilder)]
|
||||
#[builder(doc, crate_module_path=::vespid::typed_builder)]
|
||||
#item
|
||||
|
||||
impl ::vespid::support::Props for #name {
|
||||
type Builder = #builder_name;
|
||||
fn builder() -> Self::Builder {
|
||||
#name::builder()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let has_attributes = item
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| field.ident.as_ref().expect_or_abort("field.ident == None").to_string() == "attributes");
|
||||
|
||||
if has_attributes {
|
||||
tokens.extend(quote! {
|
||||
impl #builder_name {
|
||||
pub fn push_attr<A: std::fmt::Display>(mut self, attr: A) -> Self {
|
||||
self.props.attributes.push_str(&format!("{} ", attr));
|
||||
self
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
view::process(tokens, true)
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn component(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let comp = syn::parse_macro_input!(input as ComponentFn);
|
||||
quote! { #comp }.to_token_stream().into()
|
||||
}
|
||||
|
||||
struct ComponentFn {
|
||||
item: syn::ItemFn,
|
||||
}
|
||||
|
||||
impl Parse for ComponentFn {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let item = input.parse::<syn::ItemFn>()?;
|
||||
Ok(ComponentFn { item })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ComponentFn {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let item = &self.item;
|
||||
let name = &item.sig.ident;
|
||||
|
||||
let (defs, args) = match item.sig.inputs.len() {
|
||||
0 => {
|
||||
// generate empty props
|
||||
let props_name =
|
||||
syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
|
||||
(
|
||||
props(quote! {
|
||||
pub struct #props_name{}
|
||||
}),
|
||||
quote! { _props: #props_name },
|
||||
)
|
||||
}
|
||||
// match if there is a single arg of type #nameProps
|
||||
1 if matches!(item.sig.inputs.first().unwrap(), syn::FnArg::Typed(arg) if matches!(arg.ty.as_ref(), syn::Type::Path(p) if p.path.segments.last().unwrap().ident.to_string() == format!("{}Props", name))) =>
|
||||
{
|
||||
let props = item.sig.inputs.first().unwrap();
|
||||
(quote! {}, props.to_token_stream())
|
||||
}
|
||||
_ => {
|
||||
let field_defs = &item
|
||||
.sig
|
||||
.inputs
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|i| match i {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(i, "builders do not support self");
|
||||
}
|
||||
FnArg::Typed(mut t) => {
|
||||
if t.attrs.is_empty() {
|
||||
t.attrs.push(parse_quote! { #[builder(setter(into))] });
|
||||
}
|
||||
|
||||
t
|
||||
}
|
||||
})
|
||||
.collect::<Punctuated<_, Token![,]>>();
|
||||
let field_names = item
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|i| match i {
|
||||
FnArg::Receiver(_) => {
|
||||
abort!(i, "builders do not support self");
|
||||
}
|
||||
FnArg::Typed(t) => &t.pat,
|
||||
})
|
||||
.collect::<Punctuated<_, Token![,]>>();
|
||||
let props_name =
|
||||
syn::Ident::new(&format!("{}Props", name), proc_macro2::Span::call_site());
|
||||
|
||||
(
|
||||
props(quote! {
|
||||
pub struct #props_name {
|
||||
#field_defs
|
||||
}
|
||||
}),
|
||||
quote! { #props_name { #field_names }: #props_name },
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let body = &item.block;
|
||||
let output = &item.sig.output;
|
||||
let vis = &item.vis;
|
||||
|
||||
tokens.extend(quote! {
|
||||
#defs
|
||||
#[allow(non_snake_case)]
|
||||
#vis async fn #name(#args) #output {
|
||||
#body
|
||||
}
|
||||
});
|
||||
}
|
||||
pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
component::process(input)
|
||||
}
|
||||
|
|
338
vespid/macros/src/view.rs
Normal file
338
vespid/macros/src/view.rs
Normal file
|
@ -0,0 +1,338 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2_diagnostics::Diagnostic;
|
||||
use quote::{quote, quote_spanned, ToTokens};
|
||||
use rstml::{
|
||||
node::{KeyedAttribute, Node, NodeAttribute, NodeElement, NodeName},
|
||||
Infallible,
|
||||
Parser,
|
||||
ParserConfig,
|
||||
};
|
||||
use syn::{spanned::Spanned, Expr, ExprLit};
|
||||
|
||||
fn is_empty_element(name: &str) -> bool {
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
|
||||
match name {
|
||||
"img" | "input" | "meta" | "link" | "hr" | "br" | "source" | "track" | "wbr" | "area"
|
||||
| "base" | "col" | "embed" | "param" => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn empty_elements_set() -> HashSet<&'static str> {
|
||||
[
|
||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
|
||||
"source", "track", "wbr",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn process(tokens: TokenStream, ide_helper: bool) -> TokenStream {
|
||||
let config = ParserConfig::new()
|
||||
.recover_block(true)
|
||||
.element_close_use_default_wildcard_ident(true)
|
||||
.always_self_closed_elements(empty_elements_set());
|
||||
|
||||
let parser = Parser::new(config);
|
||||
let (nodes, errors) = parser.parse_recoverable(tokens).split_vec();
|
||||
process_nodes(ide_helper, &nodes, errors).into()
|
||||
}
|
||||
|
||||
fn process_nodes<'n>(
|
||||
ide_helper: bool,
|
||||
nodes: &'n Vec<Node>,
|
||||
errors: Vec<Diagnostic>,
|
||||
) -> proc_macro2::TokenStream {
|
||||
let WalkNodesOutput {
|
||||
static_format: html_string,
|
||||
values,
|
||||
collected_elements: elements,
|
||||
diagnostics,
|
||||
} = walk_nodes(&nodes);
|
||||
let docs = if ide_helper {
|
||||
generate_tags_docs(elements)
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let errors = errors
|
||||
.into_iter()
|
||||
.map(|e| e.emit_as_expr_tokens())
|
||||
.chain(diagnostics);
|
||||
quote! {
|
||||
{
|
||||
// Make sure that "compile_error!(..);" can be used in this context.
|
||||
#(#errors;)*
|
||||
// Make sure that "enum x{};" and "let _x = crate::element;" can be used in this context
|
||||
#(#docs;)*
|
||||
format!(#html_string, #(vespid::FormatRender::new(#values)),*)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_tags_docs(elements: Vec<&NodeName>) -> Vec<proc_macro2::TokenStream> {
|
||||
// Mark some of elements as type,
|
||||
// and other as elements as fn in crate::docs,
|
||||
// to give an example how to link tag with docs.
|
||||
let elements_as_type: HashSet<&'static str> =
|
||||
vec!["html", "head", "meta", "link", "body", "div"]
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
elements
|
||||
.into_iter()
|
||||
.map(|e| {
|
||||
if elements_as_type.contains(&*e.to_string()) {
|
||||
let element = quote_spanned!(e.span() => enum);
|
||||
quote!({#element X{}})
|
||||
} else {
|
||||
// let _ = crate::docs::element;
|
||||
let element = quote_spanned!(e.span() => element);
|
||||
quote!(let _ = crate::docs::#element)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct WalkNodesOutput<'a> {
|
||||
static_format: String,
|
||||
// Use proc_macro2::TokenStream instead of syn::Expr
|
||||
// to provide more errors to the end user.
|
||||
values: Vec<proc_macro2::TokenStream>,
|
||||
// Additional diagnostic messages.
|
||||
diagnostics: Vec<proc_macro2::TokenStream>,
|
||||
// Collect elements to provide semantic highlight based on element tag.
|
||||
// No differences between open tag and closed tag.
|
||||
// Also multiple tags with same name can be present,
|
||||
// because we need to mark each of them.
|
||||
collected_elements: Vec<&'a NodeName>,
|
||||
}
|
||||
impl<'a> WalkNodesOutput<'a> {
|
||||
fn extend(&mut self, other: WalkNodesOutput<'a>) {
|
||||
self.static_format.push_str(&other.static_format);
|
||||
self.values.extend(other.values);
|
||||
self.diagnostics.extend(other.diagnostics);
|
||||
self.collected_elements.extend(other.collected_elements);
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_nodes<'a>(nodes: &'a Vec<Node>) -> WalkNodesOutput<'a> {
|
||||
let mut out = WalkNodesOutput::default();
|
||||
|
||||
for node in nodes {
|
||||
match node {
|
||||
Node::Doctype(doctype) => {
|
||||
let value = &doctype.value.to_token_stream_string();
|
||||
out.static_format.push_str(&format!("<!DOCTYPE {}>", value));
|
||||
}
|
||||
Node::Element(element) => {
|
||||
let name = element.name().to_string();
|
||||
|
||||
if !is_component_tag_name(&name) {
|
||||
match element.name() {
|
||||
NodeName::Block(block) => {
|
||||
out.static_format.push_str("<{}");
|
||||
out.values.push(block.to_token_stream());
|
||||
}
|
||||
_ => {
|
||||
out.static_format.push_str(&format!("<{}", name));
|
||||
out.collected_elements.push(&element.open_tag.name);
|
||||
if let Some(e) = &element.close_tag {
|
||||
out.collected_elements.push(&e.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// attributes
|
||||
for attribute in element.attributes() {
|
||||
match attribute {
|
||||
NodeAttribute::Block(block) => {
|
||||
// If the nodes parent is an attribute we prefix with whitespace
|
||||
out.static_format.push(' ');
|
||||
out.static_format.push_str("{}");
|
||||
out.values.push(block.to_token_stream());
|
||||
}
|
||||
NodeAttribute::Attribute(attribute) => {
|
||||
let (static_format, value) = walk_attribute(attribute);
|
||||
out.static_format.push_str(&static_format);
|
||||
if let Some(value) = value {
|
||||
out.values.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ignore childs of special Empty elements
|
||||
if is_empty_element(element.open_tag.name.to_string().as_str()) {
|
||||
out.static_format.push_str(" />");
|
||||
if !element.children.is_empty() {
|
||||
let warning = proc_macro2_diagnostics::Diagnostic::spanned(
|
||||
element.open_tag.name.span(),
|
||||
proc_macro2_diagnostics::Level::Warning,
|
||||
"Element is processed as empty, and cannot have any child",
|
||||
);
|
||||
out.diagnostics.push(warning.emit_as_expr_tokens())
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
out.static_format.push('>');
|
||||
|
||||
// children
|
||||
let other_output = walk_nodes(&element.children);
|
||||
out.extend(other_output);
|
||||
|
||||
match element.name() {
|
||||
NodeName::Block(block) => {
|
||||
out.static_format.push_str("</{}>");
|
||||
out.values.push(block.to_token_stream());
|
||||
}
|
||||
_ => {
|
||||
out.static_format.push_str(&format!("</{}>", name));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// custom elements
|
||||
out.static_format.push_str("{}");
|
||||
out.values
|
||||
.push(CustomElement::new(element).to_token_stream());
|
||||
}
|
||||
}
|
||||
Node::Text(text) => {
|
||||
out.static_format.push_str(&text.value_string());
|
||||
}
|
||||
Node::RawText(text) => {
|
||||
out.static_format.push_str(&text.to_string_best());
|
||||
}
|
||||
Node::Fragment(fragment) => {
|
||||
let other_output = walk_nodes(&fragment.children);
|
||||
out.extend(other_output)
|
||||
}
|
||||
Node::Comment(comment) => {
|
||||
out.static_format.push_str("<!-- {} -->");
|
||||
out.values.push(comment.value.to_token_stream());
|
||||
}
|
||||
Node::Block(block) => {
|
||||
let block = block.try_block().unwrap();
|
||||
let stmts = &block.stmts;
|
||||
out.static_format.push_str("{}");
|
||||
out.values.push(quote!(#(#stmts)*));
|
||||
}
|
||||
Node::Custom(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn walk_attribute(attribute: &KeyedAttribute) -> (String, Option<proc_macro2::TokenStream>) {
|
||||
let mut static_format = String::new();
|
||||
let mut format_value = None;
|
||||
let key = match attribute.key.to_string().as_str() {
|
||||
"as_" => "as".to_string(),
|
||||
_ => attribute.key.to_string(),
|
||||
};
|
||||
static_format.push_str(&format!(" {}", key));
|
||||
|
||||
match attribute.value() {
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Str(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(
|
||||
r#"="{}""#,
|
||||
html_escape::encode_unquoted_attribute(&value.value())
|
||||
));
|
||||
}
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Bool(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(r#"="{}""#, value.value()));
|
||||
}
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Int(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(r#"="{}""#, value.token()));
|
||||
}
|
||||
Some(Expr::Lit(ExprLit {
|
||||
lit: syn::Lit::Float(value),
|
||||
..
|
||||
})) => {
|
||||
static_format.push_str(&format!(r#"="{}""#, value.token()));
|
||||
}
|
||||
Some(value) => {
|
||||
static_format.push_str(r#"="{}""#);
|
||||
format_value = Some(
|
||||
quote! {{
|
||||
::vespid::EscapeAttribute::escape_attribute(&#value)
|
||||
}}
|
||||
.into_token_stream(),
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
(static_format, format_value)
|
||||
}
|
||||
|
||||
fn is_component_tag_name(name: &str) -> bool {
|
||||
// either module::Component or Component
|
||||
name.chars().any(|c| c == ':' || c.is_ascii_uppercase())
|
||||
}
|
||||
|
||||
struct CustomElement<'e> {
|
||||
e: &'e NodeElement<Infallible>,
|
||||
}
|
||||
|
||||
impl<'e> CustomElement<'e> {
|
||||
fn new(e: &'e NodeElement<Infallible>) -> Self {
|
||||
CustomElement { e }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'e> ToTokens for CustomElement<'_> {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let name = self.e.name();
|
||||
|
||||
let mut chain = vec![quote! {
|
||||
::vespid::support::props_builder(&#name)
|
||||
}];
|
||||
|
||||
let children = &self.e.children;
|
||||
if !children.is_empty() {
|
||||
let c = process_nodes(false, children, vec![]);
|
||||
chain.push(quote! { .children(#c) });
|
||||
}
|
||||
|
||||
chain.push({
|
||||
self.e
|
||||
.attributes()
|
||||
.iter()
|
||||
.map(|a| match a {
|
||||
NodeAttribute::Block(block) => {
|
||||
quote! {
|
||||
.push_attr(
|
||||
#[allow(unused_braces)]
|
||||
#block
|
||||
)
|
||||
}
|
||||
}
|
||||
NodeAttribute::Attribute(attribute) => {
|
||||
let key = &attribute.key;
|
||||
let value = attribute.value().unwrap();
|
||||
quote! { .#key(#value) }
|
||||
}
|
||||
})
|
||||
.collect::<proc_macro2::TokenStream>()
|
||||
});
|
||||
|
||||
chain.push(quote! { .build() });
|
||||
|
||||
tokens.extend(quote! {
|
||||
#name(#(#chain)*).await
|
||||
});
|
||||
}
|
||||
}
|
|
@ -30,6 +30,16 @@ impl EscapeAttribute for &String {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: EscapeAttribute> EscapeAttribute for Option<T> {
|
||||
#[inline(always)]
|
||||
fn escape_attribute(&self) -> Cow<'_, str> {
|
||||
match self {
|
||||
Some(inner) => inner.escape_attribute(),
|
||||
None => Cow::Borrowed(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_escape_attribute_literal {
|
||||
($($t:ty),*) => {
|
||||
$(
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
extern crate self as vespid;
|
||||
|
||||
mod escape_attribute;
|
||||
pub use escape_attribute::*;
|
||||
|
||||
|
@ -27,13 +25,8 @@ pub extern crate html_escape;
|
|||
#[doc(hidden)]
|
||||
pub extern crate typed_builder;
|
||||
|
||||
#[cfg(feature = "icons")]
|
||||
pub mod icons;
|
||||
|
||||
pub mod prelude {
|
||||
pub use {html_escape, typed_builder};
|
||||
pub use vespid_macros::*;
|
||||
pub use crate::{context::*, render::Render, render_adapter::*, text::*};
|
||||
#[cfg(feature = "icons")]
|
||||
pub use crate::icons::*;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue