1mod kw;
2#[cfg(feature = "script")]
3mod script;
4mod url;
5
6#[cfg(feature = "script")]
7use script::EffectScriptEntry;
8
9use crate::url::{LibraryUrl, UrlInput};
10use itertools::{izip, Either};
11use proc_macro::TokenStream;
12use proc_macro2::Span;
13use quote::quote;
14
15use syn::parse::{Parse, ParseStream};
16
17use syn::{
18 parse_macro_input, Error as SynError, Expr, ExprLit, Ident, Lit, LitInt, LitStr,
19 Result as SynResult,
20};
21use yew_interop_core::LinkType;
22
23#[cfg(feature = "script")]
24use syn::Token;
25
26struct ResourceDeclaration {
27 idents: Vec<Ident>,
28 link_groups: Vec<Vec<LibraryUrl>>,
29 #[cfg(feature = "script")]
30 effect_scripts: Vec<EffectScriptEntry>,
31}
32
33#[cfg(feature = "script")]
34enum NextEntry {
35 EffectScript,
36 Lib,
37}
38
39#[cfg(feature = "script")]
40fn peek_script_or_lib(input: ParseStream) -> SynResult<NextEntry> {
41 let lookahead = input.lookahead1();
42
43 lookahead
44 .peek(Token![!])
45 .then(|| NextEntry::EffectScript)
46 .or_else(|| lookahead.peek(Ident).then(|| NextEntry::Lib))
47 .ok_or_else(|| lookahead.error())
48}
49
50fn parse_library_urls(input: ParseStream) -> SynResult<Vec<LibraryUrl>> {
51 let mut urls = Vec::new();
52 loop {
53 if input.peek(kw::js) {
54 input.parse::<kw::js>().unwrap();
55 let expr = input.parse::<Expr>()?;
56 urls.push(LibraryUrl::new(
57 UrlInput::TypeSpecified(Box::new(expr)),
58 LinkType::Js,
59 ))
60 } else if input.peek(kw::css) {
61 input.parse::<kw::css>().unwrap();
62 let expr = input.parse::<Expr>()?;
63 urls.push(LibraryUrl::new(
64 UrlInput::TypeSpecified(Box::new(expr)),
65 LinkType::Css,
66 ))
67 } else if input.peek(LitStr) {
68 urls.push(LibraryUrl::try_from(input.parse::<LitStr>().unwrap())?);
69 } else {
70 break;
71 }
72 }
73 Ok(urls)
74}
75
76impl Parse for ResourceDeclaration {
77 fn parse(input: ParseStream) -> SynResult<Self> {
78 let mut idents = Vec::new();
79 let mut link_groups = Vec::new();
80
81 #[cfg(feature = "script")]
82 let mut effect_scripts = Vec::new();
83
84 while !input.is_empty() {
85 #[cfg(feature = "script")]
86 match peek_script_or_lib(input)? {
87 NextEntry::EffectScript => {
88 let entry = EffectScriptEntry::parse(input)?;
89 effect_scripts.push(entry)
90 }
91 NextEntry::Lib => {
92 Self::parse_library(input, &mut idents, &mut link_groups)?;
93 }
94 }
95 #[cfg(not(feature = "script"))]
96 Self::parse_library(input, &mut idents, &mut link_groups)?;
97 }
98
99 Ok(Self {
100 idents,
101 link_groups,
102
103 #[cfg(feature = "script")]
104 effect_scripts,
105 })
106 }
107}
108
109impl ResourceDeclaration {
110 fn parse_library(
111 input: ParseStream,
112 idents: &mut Vec<Ident>,
113 link_groups: &mut Vec<Vec<LibraryUrl>>,
114 ) -> SynResult<()> {
115 let ident = input.parse::<Ident>().unwrap();
116 idents.push(ident);
117 link_groups.push(parse_library_urls(input)?);
118 Ok(())
119 }
120}
121
122#[proc_macro]
235pub fn declare_resources(input: TokenStream) -> TokenStream {
236 let resource_declaration = parse_macro_input!(input as ResourceDeclaration);
237
238 #[cfg(not(feature = "script"))]
239 let ResourceDeclaration {
240 idents,
241 link_groups,
242 } = resource_declaration;
243
244 #[cfg(feature = "script")]
245 let ResourceDeclaration {
246 idents,
247 link_groups,
248 effect_scripts,
249 } = resource_declaration;
250
251 #[cfg(feature = "script")]
252 let (script_hooks, script_handle_enums, script_urls, script_loaders, script_handles): (
253 Vec<_>,
254 Vec<_>,
255 Vec<_>,
256 Vec<_>,
257 Vec<_>,
258 ) = itertools::multiunzip(effect_scripts.into_iter().map(
259 |EffectScriptEntry { ident, url }| {
260 let ident_string = ident.to_string();
261
262 (
263 Ident::new(&format!("use_{}", ident_string), ident.span()),
264 Ident::new(&format!("{}ScriptHandle", ident_string), Span::call_site()),
265 url,
266 Ident::new(
267 &format!("{}_script_loader", ident_string),
268 Span::call_site(),
269 ),
270 Ident::new(
271 &format!("{}_script_handle", ident_string),
272 Span::call_site(),
273 ),
274 )
275 },
276 ));
277
278 let (resource_names, resource_name_spans): (Vec<_>, Vec<_>) =
279 idents.iter().map(|i| (i.to_string(), i.span())).unzip();
280
281 let handle_idents = resource_names
282 .iter()
283 .map(|name| Ident::new(&format!("{}LinkGroupStatusHandle", name), Span::call_site()));
284
285 let library_hooks = izip!(
286 resource_names.iter(),
287 resource_name_spans,
288 link_groups.iter(),
289 handle_idents.clone()
290 )
291 .map(|(resource_name, span, links, handle_ident)| {
292 let ident = format!("use_{}", resource_name);
293 let ident = Ident::new(&ident, span);
294
295 let links = links.iter().map(|LibraryUrl { link_type, url }| {
296 let r#type = match link_type {
297 LinkType::Css => quote! {Css},
298 LinkType::Js => quote! {Js},
299 };
300
301 quote! {
302 yew_interop::Link {
303 r#type: ::yew_interop::LinkType::#r#type,
304 src: ::std::borrow::Cow::from(#url),
305 }
306 }
307 });
308 let handle_ident_one = handle_ident.clone();
309 let handle_ident_two = handle_ident.clone();
310 let handle_ident_three = handle_ident.clone();
311
312 quote! {
313
314 pub fn #ident() -> bool{
319 let handle = ::yew::use_context::<#handle_ident_one>().unwrap();
320 match handle {
321 #handle_ident_two::NotRequested(disp) => {
322 disp.dispatch(::yew_interop::LinkGroupStatusAction::PleaseStart(vec![
323 #(
324 #links,
325 )*
326 ]));
327 false
328 }
329 #handle_ident_three::Started => false,
330 #handle_ident::Completed => true
331 }
332 }
333 }
334 });
335
336 #[cfg(feature = "script")]
337 let script_hooks = {
338 let script_handle_enums_one = script_handle_enums.iter();
339 let script_handle_enums_two = script_handle_enums_one.clone();
340 let script_handle_enums_three = script_handle_enums_one.clone();
341 let script_handle_enums_four = script_handle_enums_one.clone();
342 quote! {
343
344 #(
345 pub fn #script_hooks() -> Option<::yew_interop::script::Script> {
349 let handle = ::yew::use_context::<#script_handle_enums_four>().unwrap();
350 match handle {
351 #script_handle_enums_one::NotRequested(disp) => {
352 disp.dispatch(::yew_interop::script::ScriptLoaderAction::Start);
353 ::yew_interop::script::wasm_bindgen_futures::spawn_local(async move {
354 let script = ::yew_interop::script::fetch_script(#script_urls.into()).await;
355 disp.dispatch(::yew_interop::script::ScriptLoaderAction::Finish(
356 ::std::rc::Rc::new(script),
357 ));
358 });
359 None
360 }
361 #script_handle_enums_two::Started => None,
362 #script_handle_enums_three::Completed(s) => Some(s),
363 }
364 }
365 )*
366
367
368 }
369 };
370 #[cfg(not(feature = "script"))]
371 let script_hooks = quote! {};
372
373 #[cfg(feature = "script")]
374 let script_handle_enums_ts = {
375 let script_handle_enums = script_handle_enums.iter();
376 quote! {
377
378 #(
379 #[derive(Clone, PartialEq)]
380 enum #script_handle_enums {
381 NotRequested(::yew::UseReducerDispatcher<::yew_interop::script::ScriptLoader>),
382 Started,
383 Completed(::yew_interop::script::Script),
384 }
385 )*
386
387 }
388 };
389 #[cfg(not(feature = "script"))]
390 let script_handle_enums_ts = quote! {};
391
392 #[cfg(feature = "script")]
393 let script_loaders_and_handles = {
394 let script_loaders_one = script_loaders.iter();
395 let script_loaders_two = script_loaders_one.clone();
396 let script_handle_enums = script_handle_enums.iter();
397 let script_handle_enums_one = script_handle_enums.clone();
398 let script_handle_enums_two = script_handle_enums.clone();
399 let script_handles = script_handles.iter();
400 quote! {
401
402 #(
403
404 let #script_loaders =
405 ::yew::use_reducer(|| ::yew_interop::script::ScriptLoader::NotRequested);
406 let #script_handles = match &*#script_loaders_one {
407 yew_interop::script::ScriptLoader::NotRequested => {
408 #script_handle_enums_two::NotRequested(#script_loaders_two.dispatcher())
409 }
410 yew_interop::script::ScriptLoader::Started => #script_handle_enums::Started,
411 yew_interop::script::ScriptLoader::Completed(s) => {
412 #script_handle_enums_one::Completed(s.clone())
413 }
414 };
415
416 )*
417
418
419 }
420 };
421 #[cfg(not(feature = "script"))]
422 let script_loaders_and_handles = quote! {};
423
424 let handle_idents_one = handle_idents.clone();
425 let handle_idents_two = handle_idents.clone();
426 let handle_idents_three = handle_idents.clone();
427 let handle_idents_four = handle_idents.clone();
428 let handle_idents_five = handle_idents.clone().rev();
429
430 let handle_enums = handle_idents.clone().map(|handle_ident| {
431 quote! {
432 #[derive(Clone, PartialEq)]
433 enum #handle_ident {
434 NotRequested(::yew::UseReducerDispatcher<::yew_interop::LinkGroupStatus>),
435 Started,
436 Completed,
437 }
438 }
439 });
440
441 let remaining_idents = resource_names
442 .iter()
443 .map(|name| Ident::new(&format!("{}_REMAINING", name), Span::call_site()));
444 let remaining_idents_one = remaining_idents.clone();
445 let remaining_count = link_groups
446 .iter()
447 .map(|link_group| LitInt::new(&link_group.len().to_string(), Span::call_site()));
448
449 let reducer_idents = resource_names
450 .iter()
451 .map(|name| Ident::new(&format!("{}_link_group_status", name), Span::call_site()));
452
453 let handle_tmp_idents = resource_names
454 .iter()
455 .map(|name| Ident::new(&format!("{}_link_handle", name), Span::call_site()));
456
457 let handle_tmp_idents_one = handle_tmp_idents.clone();
458
459 let link_element_tmp_idents = resource_names
460 .iter()
461 .map(|name| Ident::new(&format!("{}_links", name), Span::call_site()));
462
463 let link_element_tmp_idents_one = link_element_tmp_idents.clone();
464 let reducer_idents_one = reducer_idents.clone();
465 let reducer_idents_two = reducer_idents.clone();
466 let reducer_idents_three = reducer_idents.clone();
467
468 let expanded = {
469 #[cfg(feature = "script")]
470 let script_context_opening_tags = {
471 let script_handle_enums = script_handle_enums.iter();
472
473 quote! {
474 #(
475 <::yew::ContextProvider<#script_handle_enums> context={#script_handles}>
476 )*
477 }
478 };
479 #[cfg(not(feature = "script"))]
480 let script_context_opening_tags = quote! {};
481
482 #[cfg(feature = "script")]
483 let script_context_closing_tags = {
484 let script_handle_enums = script_handle_enums.into_iter().rev();
485 quote! {
486 #(
487 </::yew::ContextProvider<#script_handle_enums>>
488 )*
489 }
490 };
491 #[cfg(not(feature = "script"))]
492 let script_context_closing_tags = quote! {};
493
494 quote! {
495
496 #(
497 #library_hooks
498 )*
499
500 #script_hooks
501
502 #script_handle_enums_ts
503
504 #(
505 #handle_enums
506 )*
507
508 #[derive(::yew::Properties, PartialEq)]
509 pub struct ResourceProviderProps {
510 pub children: ::yew::Children,
511 }
512
513 #[::yew::function_component(ResourceProvider)]
514 pub fn resource_provider(props: &ResourceProviderProps) -> Html {
515 #(
516 let #reducer_idents = ::yew::use_reducer(::yew_interop::LinkGroupStatus::default);
517 )*
518
519 thread_local!{
520 #(
521 static #remaining_idents: ::std::cell::RefCell<u8> = ::std::cell::RefCell::new(#remaining_count);
522 )*
523 }
524
525 let make_links_with_onload = |links: Vec<::yew_interop::Link>, onload: Option<::yew::Callback<::yew::Event>>|{
526 ::yew::html!{
527 <>
528 {for
529 links.into_iter().map(|link| {
530
531 let src: ::yew::virtual_dom::AttrValue = link.src.clone().into();
532 match link.r#type {
533 ::yew_interop::LinkType::Js => ::yew::html! {
534 <script {src} type="text/javascript" onload={onload.clone()}/>
535 },
536 ::yew_interop::LinkType::Css => ::yew::html! {
537 <link rel="stylesheet" type="text/css" href={src} onload={onload.clone()}/>
538 }
539 }
540 }
541 )
542 }
543 </>
544 }
545 };
546
547 let get_links =
548 |links: &Vec<::yew_interop::Link>,
549 link_group_status: &::yew::UseReducerHandle<::yew_interop::LinkGroupStatus>,
550 count: &'static ::std::thread::LocalKey<::std::cell::RefCell<u8>>| {
551 let dispatcher = link_group_status.dispatcher();
552 let onload = move |_| {
553 count.with(|r| {
554 let current = *r.borrow();
555 *r.borrow_mut() = current - 1;
556 if current > 1 {
557 *r.borrow_mut() = current - 1
558 } else {
559 dispatcher.dispatch(::yew_interop::LinkGroupStatusAction::Completed)
560 }
561 })
562 };
563 make_links_with_onload(links.clone(), Some(::yew::Callback::from(onload)))
564 };
565
566
567 #(
568 let (#handle_tmp_idents, #link_element_tmp_idents) = match &*#reducer_idents_one {
569 ::yew_interop::LinkGroupStatus::Started {links} => (
570 #handle_idents_one::Started,
571 get_links(links, &#reducer_idents_two, &#remaining_idents_one)
572 ),
573 ::yew_interop::LinkGroupStatus::Completed{links} => {
574 (#handle_idents_two::Completed,
575 make_links_with_onload(links.clone(), None))},
576 ::yew_interop::LinkGroupStatus::NotRequested => (#handle_idents_three::NotRequested(#reducer_idents_three.dispatcher()), ::yew::html!{})
577 };
578 )*
579
580 #script_loaders_and_handles
581
582 ::yew::html! {
583 <>
584 #(
585 <::yew::ContextProvider<#handle_idents_four> context={#handle_tmp_idents_one}>
586 )*
587
588 #script_context_opening_tags
589 {for props.children.iter()}
590 #script_context_closing_tags
591
592 #(
593 </::yew::ContextProvider<#handle_idents_five>>
594 )*
595 #(
600 {#link_element_tmp_idents_one}
601 )*
602 </>
603 }
604 }
605
606
607 }
608 };
609
610 expanded.into()
611}
612
613trait ExprExt {
614 fn into_lit_str(self) -> SynResult<Either<LitStr, Expr>>;
615}
616
617impl ExprExt for syn::Expr {
618 fn into_lit_str(self) -> SynResult<Either<LitStr, Self>> {
619 match self {
620 syn::Expr::Lit(ExprLit { lit, .. }) => match lit {
621 Lit::Str(lit_str) => Ok(Either::Left(lit_str)),
622 _ => Err(SynError::new(lit.span(), "expecting a string literal")),
623 },
624 other => Ok(Either::Right(other)),
625 }
626 }
627}