1use parse::Transpile;
2use proc_macro::TokenStream;
3use proc_macro2::Span;
4use proc_macro_error::{abort, abort_call_site, proc_macro_error};
5use quote::quote;
6use silkenweb_css::{Css, NameMapping};
7use syn::{
8 parse_macro_input, Attribute, Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed,
9 FieldsUnnamed, Ident, Index, LitBool,
10};
11
12use crate::parse::Input;
13
14mod parse;
15
16macro_rules! derive_empty(
17 (
18 $($proc_name:ident ( $type_path:path, $type_name:ident ); )*
19 ) => {$(
20 #[doc = concat!("Derive `", stringify!($type_name), "`")]
21 #[doc = ""]
22 #[doc = "This will derive an instance with an empty body:"]
23 #[doc = ""]
24 #[doc = concat!("`impl ", stringify!($type_name), " for MyType {}`")]
25 #[doc = ""]
26 #[doc = "Types with generic parameters are supported."]
27 #[proc_macro_derive($type_name)]
28 pub fn $proc_name(item: TokenStream) -> TokenStream {
29 let item: DeriveInput = parse_macro_input!(item);
30 let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
31 let name = item.ident;
32
33 quote!(
34 impl #impl_generics ::silkenweb::$type_path::$type_name
35 for #name #ty_generics #where_clause {}
36 ).into()
37 }
38 )*}
39);
40
41derive_empty!(
42 derive_value(value, Value);
43 derive_html_element(elements, HtmlElement);
44 derive_aria_element(elements, AriaElement);
45 derive_html_element_events(elements, HtmlElementEvents);
46 derive_element_events(elements, ElementEvents);
47);
48
49#[proc_macro_derive(StrAttribute)]
50pub fn str_attribute(item: TokenStream) -> TokenStream {
51 let item: DeriveInput = parse_macro_input!(item);
52 let item_name = item.ident;
53 let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
54
55 quote!(
56 impl #impl_generics ::silkenweb::attribute::Attribute
57 for #item_name #ty_generics #where_clause {
58 type Text<'a> = &'a str;
59
60 fn text(&self) -> Option<<Self as ::silkenweb::attribute::Attribute>::Text<'_>> {
61 Some(self.as_ref())
62 }
63 }
64
65 impl #impl_generics ::silkenweb::attribute::AsAttribute<#item_name>
66 for #item_name #ty_generics #where_clause {}
67 )
68 .into()
69}
70
71#[proc_macro_derive(ChildElement, attributes(child_element))]
72#[proc_macro_error]
73pub fn derive_child_element(item: TokenStream) -> TokenStream {
74 let item: DeriveInput = parse_macro_input!(item);
75 let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
76 let item_name = item.ident;
77
78 let fields = fields(item.data);
79 let target_index = target_field_index("child_element", &fields);
80
81 let target_field = fields[target_index].clone();
82 let target_type = target_field.ty;
83 let target = field_token(target_index, target_field.ident);
84 let dom_type = quote!(<#target_type as ::silkenweb::dom::InDom>::Dom);
85
86 quote!(
87 impl #impl_generics ::std::convert::From<#item_name #ty_generics>
88 for ::silkenweb::node::element::GenericElement<
89 #dom_type,
90 ::silkenweb::node::element::Const
91 >
92 #where_clause
93 {
94 fn from(value: #item_name #ty_generics) -> Self {
95 value.#target.into()
96 }
97 }
98
99 impl #impl_generics ::std::convert::From<#item_name #ty_generics>
100 for ::silkenweb::node::Node<#dom_type>
101 #where_clause
102 {
103 fn from(value: #item_name #ty_generics) -> Self {
104 value.#target.into()
105 }
106 }
107
108 impl #impl_generics ::silkenweb::value::Value
109 for #item_name #ty_generics #where_clause {}
110 )
111 .into()
112}
113
114#[proc_macro_derive(Element, attributes(element))]
115#[proc_macro_error]
116pub fn derive_element(item: TokenStream) -> TokenStream {
117 let item: DeriveInput = parse_macro_input!(item);
118 let (impl_generics, ty_generics, where_clause) = item.generics.split_for_impl();
119 let item_name = item.ident;
120
121 let fields = fields(item.data);
122 let target_index = target_field_index("element", &fields);
123
124 let field = fields[target_index].clone();
125 let target_type = field.ty;
126
127 let other_field_idents = fields.into_iter().enumerate().filter_map(|(index, field)| {
128 (index != target_index).then(|| field_token(index, field.ident))
129 });
130 let other_fields = quote!(#(, #other_field_idents: self.#other_field_idents)*);
131
132 let target = field_token(0, field.ident);
133
134 quote!(
135 impl #impl_generics ::silkenweb::node::element::Element
136 for #item_name #ty_generics #where_clause {
137 type Dom = <#target_type as ::silkenweb::node::element::Element>::Dom;
138 type DomElement = <#target_type as ::silkenweb::node::element::Element>::DomElement;
139
140 fn class<'a, T>(self, class: impl ::silkenweb::value::RefSignalOrValue<'a, Item = T>) -> Self
141 where
142 T: 'a + AsRef<str>
143 {
144 Self {#target: self.#target.class(class) #other_fields}
145 }
146
147 fn classes<'a, T, Iter>(
148 self,
149 classes: impl ::silkenweb::value::RefSignalOrValue<'a, Item = Iter>,
150 ) -> Self
151 where
152 T: 'a + AsRef<str>,
153 Iter: 'a + IntoIterator<Item = T>,
154 {
155 Self {#target: self.#target.classes(classes) #other_fields}
156 }
157
158 fn attribute<'a>(
159 mut self,
160 name: &str,
161 value: impl ::silkenweb::value::RefSignalOrValue<'a, Item = impl ::silkenweb::attribute::Attribute>,
162 ) -> Self {
163 Self{#target: self.#target.attribute(name, value) #other_fields}
164 }
165
166 fn style_property<'a>(
167 self,
168 name: impl Into<String>,
169 value: impl ::silkenweb::value::RefSignalOrValue<'a, Item = impl AsRef<str> + 'a>
170 ) -> Self {
171 Self{#target: self.#target.style_property(name, value) #other_fields}
172 }
173
174 fn effect(self, f: impl FnOnce(&Self::DomElement) + 'static) -> Self {
175 Self{#target: self.#target.effect(f) #other_fields}
176 }
177
178 fn effect_signal<T: 'static>(
179 self,
180 sig: impl ::silkenweb::macros::Signal<Item = T> + 'static,
181 f: impl Fn(&Self::DomElement, T) + Clone + 'static,
182 ) -> Self {
183 Self{#target: self.#target.effect_signal(sig, f) #other_fields}
184 }
185
186 fn map_element(self, f: impl FnOnce(&Self::DomElement) + 'static) -> Self {
187 Self{#target: self.#target.map_element(f) #other_fields}
188 }
189
190 fn map_element_signal<T: 'static>(
191 self,
192 sig: impl ::silkenweb::macros::Signal<Item = T> + 'static,
193 f: impl Fn(&Self::DomElement, T) + Clone + 'static,
194 ) -> Self {
195 Self{#target: self.#target.map_element_signal(sig, f) #other_fields}
196 }
197
198 fn handle(&self) -> ::silkenweb::node::element::ElementHandle<Self::Dom, Self::DomElement> {
199 self.#target.handle()
200 }
201
202 fn spawn_future(self, future: impl ::std::future::Future<Output = ()> + 'static) -> Self {
203 Self{#target: self.#target.spawn_future(future) #other_fields}
204 }
205
206 fn on(self, name: &'static str, f: impl FnMut(::silkenweb::macros::JsValue) + 'static) -> Self {
207 Self{#target: self.#target.on(name, f) #other_fields}
208 }
209 }
210 )
211 .into()
212}
213
214fn target_field_index(attr_name: &str, fields: &[Field]) -> usize {
216 let mut target_index = None;
217
218 for (index, field) in fields.iter().enumerate() {
219 for attr in &field.attrs {
220 if target_index.is_some() {
221 abort!(attr, "Only one target field can be specified");
222 }
223
224 check_attr_matches(attr, attr_name, "target");
225 target_index = Some(index);
226 }
227 }
228
229 target_index.unwrap_or_else(|| {
230 if fields.len() != 1 {
231 abort_call_site!(
232 "There must be exactly one field, or specify `#[{}(target)]` on a single field",
233 attr_name
234 );
235 }
236
237 0
238 })
239}
240
241fn check_attr_matches(attr: &Attribute, name: &str, value: &str) {
243 let path = attr.path();
244
245 if !path.is_ident(name) {
246 abort!(path, "Expected `{}`", name);
247 }
248
249 attr.parse_nested_meta(|meta| {
250 if !meta.path.is_ident(value) {
251 abort!(meta.path, "Expected `{}`", value);
252 }
253
254 if !meta.input.is_empty() {
255 abort!(meta.input.span(), "Unexpected token");
256 }
257
258 Ok(())
259 })
260 .unwrap()
261}
262
263fn fields(struct_data: Data) -> Vec<Field> {
264 let fields = match struct_data {
265 Data::Struct(DataStruct { fields, .. }) => fields,
266 _ => abort_call_site!("Only structs are supported"),
267 };
268
269 match fields {
270 Fields::Unnamed(FieldsUnnamed { unnamed, .. }) => unnamed,
271 Fields::Named(FieldsNamed { named, .. }) => named,
272 Fields::Unit => abort!(fields, "There must be at least one field"),
273 }
274 .into_iter()
275 .collect()
276}
277
278fn field_token(index: usize, ident: Option<Ident>) -> proc_macro2::TokenStream {
279 if let Some(ident) = ident {
280 quote!(#ident)
281 } else {
282 let index = Index::from(index);
283 quote!(#index)
284 }
285}
286
287#[proc_macro_attribute]
288#[proc_macro_error]
289pub fn cfg_browser(attr: TokenStream, item: TokenStream) -> TokenStream {
290 let in_browser: LitBool = parse_macro_input!(attr);
291
292 let cfg_check = if in_browser.value() {
293 quote!(#[cfg(all(target_arch = "wasm32", target_os = "unknown"))])
294 } else {
295 quote!(#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))])
296 };
297 let item = proc_macro2::TokenStream::from(item);
298
299 quote!(
300 #cfg_check
301 #item
302 )
303 .into()
304}
305
306#[proc_macro]
307#[proc_macro_error]
308pub fn css(input: TokenStream) -> TokenStream {
309 let Input {
310 mut source,
311 public,
312 prefix,
313 include_prefixes,
314 exclude_prefixes,
315 validate,
316 auto_mount,
317 transpile,
318 } = parse_macro_input!(input);
319
320 let name_mappings = source
321 .transpile(validate, transpile.map(Transpile::into))
322 .unwrap_or_else(|e| abort_call_site!(e));
323
324 let variables = source.variable_names().map(|variable| NameMapping {
325 plain: variable.clone(),
326 mangled: format!("--{variable}"),
327 });
328
329 let classes = name_mappings.unwrap_or_else(|| {
330 source
331 .class_names()
332 .map(|class| NameMapping {
333 plain: class.clone(),
334 mangled: class,
335 })
336 .collect()
337 });
338
339 let classes = only_matching_prefixes(&include_prefixes, &exclude_prefixes, classes.into_iter());
340 let variables = only_matching_prefixes(&include_prefixes, &exclude_prefixes, variables);
341
342 if let Some(prefix) = prefix {
343 let classes = strip_prefixes(&prefix, classes);
344 let variables = strip_prefixes(&prefix, variables);
345 code_gen(&source, public, auto_mount, classes, variables)
346 } else {
347 code_gen(&source, public, auto_mount, classes, variables)
348 }
349}
350
351fn only_matching_prefixes<'a>(
352 include_prefixes: &'a Option<Vec<String>>,
353 exclude_prefixes: &'a [String],
354 names: impl Iterator<Item = NameMapping> + 'a,
355) -> impl Iterator<Item = NameMapping> + 'a {
356 names.filter(move |mapping| {
357 let ident = &mapping.plain;
358
359 let include = if let Some(include_prefixes) = include_prefixes.as_ref() {
360 any_prefix_matches(ident, include_prefixes)
361 } else {
362 true
363 };
364
365 let exclude = any_prefix_matches(ident, exclude_prefixes);
366
367 include && !exclude
368 })
369}
370
371fn strip_prefixes<'a>(
372 prefix: &'a str,
373 names: impl Iterator<Item = NameMapping> + 'a,
374) -> impl Iterator<Item = NameMapping> + 'a {
375 names.filter_map(move |NameMapping { plain, mangled }| {
376 plain
377 .strip_prefix(prefix)
378 .map(str::to_string)
379 .map(|plain| NameMapping { plain, mangled })
380 })
381}
382
383fn any_prefix_matches(x: &str, prefixes: &[String]) -> bool {
384 prefixes.iter().any(|prefix| x.starts_with(prefix))
385}
386
387fn code_gen(
388 source: &Css,
389 public: bool,
390 auto_mount: bool,
391 classes: impl Iterator<Item = NameMapping>,
392 variables: impl Iterator<Item = NameMapping>,
393) -> TokenStream {
394 let classes = classes.map(|name| define_css_entity(name, auto_mount));
395 let variables = variables.map(|name| define_css_entity(name, auto_mount));
396
397 let dependency = source.dependency().into_iter();
398 let content = source.content();
399 let visibility = if public { quote!(pub) } else { quote!() };
400
401 quote!(
402 #(const _: &[u8] = ::std::include_bytes!(#dependency);)*
403
404 #visibility mod class {
405 #(#classes)*
406 }
407
408 #visibility mod var {
409 #(#variables)*
410 }
411
412 #visibility mod stylesheet {
413 use ::silkenweb::document::Document;
414
415 pub fn mount() {
416 use ::silkenweb::dom::DefaultDom;
417 mount_dom::<DefaultDom>();
418 }
419
420 pub fn mount_dom<D: Document>() {
421 use ::std::{panic::Location, sync::Once};
422 use ::silkenweb::{
423 document::{Document, DocumentHead},
424 node::element::TextParentElement,
425 elements::html::style,
426 };
427
428 static INIT: Once = Once::new();
429
430 INIT.call_once(|| {
431 let location = Location::caller();
432 let head = DocumentHead::new().child(style().text(text()));
433
434 D::mount_in_head(
435 &format!(
436 "silkenweb-style:{}:{}:{}",
437 location.file(),
438 location.line(),
439 location.column()
440 ),
441 head
442 );
443 });
444 }
445
446 pub fn text() -> &'static str {
447 #content
448 }
449 }
450 )
451 .into()
452}
453
454fn define_css_entity(name: NameMapping, auto_mount: bool) -> proc_macro2::TokenStream {
455 let NameMapping { plain, mangled } = name;
456
457 if !plain.starts_with(char::is_alphabetic) {
458 abort_call_site!(
459 "Identifier '{}' doesn't start with an alphabetic character",
460 plain
461 );
462 }
463
464 let ident = plain.replace(|c: char| !c.is_alphanumeric(), "_");
465
466 if auto_mount {
467 let ident = Ident::new(&ident.to_lowercase(), Span::call_site());
468 quote!(pub fn #ident() -> &'static str {
469 super::stylesheet::mount();
470 #mangled
471 })
472 } else {
473 let ident = Ident::new(&ident.to_uppercase(), Span::call_site());
474 quote!(pub const #ident: &str = #mangled;)
475 }
476}
477
478#[doc(hidden)]
481#[proc_macro]
482#[proc_macro_error]
483pub fn rust_to_html_ident(input: TokenStream) -> TokenStream {
484 let rust_ident: Ident = parse_macro_input!(input);
485 let html_ident = rust_ident.to_string().replace('_', "-");
486 let html_ident_name = html_ident.strip_prefix("r#").unwrap_or(&html_ident);
487
488 quote!(#html_ident_name).into()
489}