Skip to main content

ifengine_macros/
lib.rs

1//! See [`ifengine::elements`]
2use proc_macro::TokenStream;
3use quote::quote;
4use syn::{
5    Arm, Error, Expr, ExprClosure, ItemFn, LitStr, Result, Token,
6    parse::{Parse, ParseStream},
7    parse_macro_input,
8    punctuated::Punctuated,
9};
10mod nodes;
11use nodes::*;
12
13/// Decorate your function with this.
14///
15/// The function must take your game state as a parameter, and return `()`.
16/// This macro will rewrite your function to receive a &mut [`ifengine::Game`] and return a [`ifengine::core::Response`], as well as enabling usage of [`ifengine::elements`] to produce that response (which in most cases will be a [`ifengine::View`]).
17///
18/// # Examples
19///```rust
20/// #[ifview]
21/// pub fn p1(s: &mut State) {
22///     h!("SALTWRACK", 3); // heading level 3
23///     p!(link!("BEGIN", p2)); // Link to the next page
24/// }
25///
26/// // ----- mod.rs -----
27/// pub type Game = ifengine::Game<State>;
28/// pub fn new() -> Game {
29///    ifengine::Game!(chap1::p1)
30/// }
31///```
32#[proc_macro_attribute]
33pub fn ifview(_attr: TokenStream, item: TokenStream) -> TokenStream {
34    let input = parse_macro_input!(item as ItemFn);
35
36    let name = &input.sig.ident;
37    let original_block = &input.block;
38
39    if input.sig.inputs.len() != 1 {
40        return Error::new_spanned(
41            &input.sig.inputs,
42            "ifview functions must have exactly one input: the context type C",
43        )
44        .to_compile_error()
45        .into();
46    }
47
48    let ctx_arg = input.sig.inputs.first().unwrap();
49    let ctx_type = if let syn::FnArg::Typed(pat_type) = ctx_arg
50        && let syn::Type::Reference(ty_ref) = &*pat_type.ty
51        && ty_ref.mutability.is_some()
52    {
53        &*ty_ref.elem
54    } else {
55        return Error::new_spanned(ctx_arg, "Expected a &mut C type")
56            .to_compile_error()
57            .into();
58    };
59
60    let expanded = quote! {
61        pub fn #name(__ifengine_game: &mut ifengine::Game<#ctx_type>)
62        -> ifengine::core::Response
63        {
64            let __ifengine_simulating = __ifengine_game.simulating();
65            #[allow(unused_variables)]
66            let #ctx_arg = &mut __ifengine_game.context;
67            let __ifengine_game_tags = &mut __ifengine_game.tags;
68            let __ifengine_game = &mut __ifengine_game.inner;
69            let mut __ifengine_page_state = ifengine::core::PageState::new(
70
71                format!("{}::{}", module_path!(), stringify!(#name)),
72                __ifengine_game.fresh(),
73                __ifengine_simulating,
74                __ifengine_game.state.get_page_mut(format!("{}::{}", module_path!(), stringify!(#name))),
75                __ifengine_game_tags,
76
77            );
78
79            #original_block
80
81            #[allow(unreachable_code)]
82            __ifengine_page_state.into_response()
83        }
84    };
85
86    expanded.into()
87}
88
89// ----------- CHOICES -------------------------
90
91// Expr instead of Pattern
92struct LineArm {
93    line: Expr,
94    block: Option<Expr>,
95}
96
97impl Parse for LineArm {
98    fn parse(input: ParseStream) -> Result<Self> {
99        let line: Expr = input.parse()?;
100
101        let block = if input.parse::<Token![=>]>().is_ok() {
102            Some(input.parse()?)
103        } else {
104            None
105        };
106
107        Ok(LineArm { line, block })
108    }
109}
110struct ChoiceInput {
111    maybe_key: MaybeKey,
112    arms: Vec<LineArm>,
113}
114
115impl Parse for ChoiceInput {
116    fn parse(input: ParseStream) -> Result<Self> {
117        let maybe_key = input.parse()?;
118
119        let mut arms = Vec::new();
120        while !input.is_empty() {
121            let mut lhs_exprs = vec![input.parse::<Expr>()?];
122
123            while input.peek(Token![|]) {
124                let _ = input.parse::<Token![|]>()?;
125                lhs_exprs.push(input.parse()?);
126            }
127
128            let block = if input.parse::<Token![=>]>().is_ok() {
129                Some(input.parse()?)
130            } else {
131                None
132            };
133
134            for line in lhs_exprs {
135                arms.push(LineArm {
136                    line,
137                    block: block.clone(),
138                });
139            }
140
141            input.parse::<Token![,]>().ok();
142        }
143
144        Ok(ChoiceInput { maybe_key, arms })
145    }
146}
147
148/// Conditionally display one of several choices based on user selection.
149///
150/// Returns true if it has resolved, otherwise false.
151///
152/// # Description
153/// The `choice!` macro takes a list of arms in the form `LHS => RHS`, where both
154/// sides implement `Into<`[`Line`](ifengine::view::Line)`>`. It works as follows:
155///
156/// - If no arm is selected, the LHS values are displayed as a list of lines.
157/// - Once a choice is selcted, subsequent renders execute the corresponding RHS expression and
158///   display its result.
159///
160/// # Additional
161/// A [`MaybeKey`] can be specified as the first argument
162///   When a choice is clicked, it sets the value of its key to (the u8 value of) its id in [`PageState`].
163///   It is discouraged to specify this: by default, it will be automatically generated.
164/// Multiple LHS values can be specified for the same RHS using `|`
165///
166/// # Example
167/// ```rust
168/// choice! {
169///     "1" => "Chose 1",
170///     "2" | "3" => {
171///         "Chose 2 or 3"
172///     },
173/// };
174/// ```
175#[proc_macro]
176pub fn choice(input: TokenStream) -> TokenStream {
177    let ChoiceInput { maybe_key, arms } = syn::parse_macro_input!(input as ChoiceInput);
178
179    let key_tokens = maybe_key.into_tokens();
180
181    let mut index_arms = Vec::new();
182    let mut lines = Vec::new();
183
184    for (i, LineArm { line, block }) in arms.iter().enumerate() {
185        let i = i as u8;
186
187        lines.push(quote! { (#i, ifengine::view::Line::from(#line)) });
188
189        let block_tokens = match block {
190            Some(b) => quote! { ifengine::view::Line::from({ #b }) },
191            None => quote! { unreachable!() },
192        };
193
194        index_arms.push(quote! {
195            #i => { #block_tokens }
196        });
197    }
198
199    let expanded = quote! {
200        if let Some(__ifengine_tmp_idx) = __ifengine_page_state.get_mask_last(#key_tokens) {
201            #[allow(unreachable_code)]
202            __ifengine_page_state.push(
203                ifengine::view::Object::Paragraph(
204                    match __ifengine_tmp_idx {
205                        #(#index_arms),*,
206                        _ => unreachable!(),
207                    }
208                )
209            );
210            true
211        } else {
212            __ifengine_page_state.push(
213                ifengine::view::Object::Choice(
214                    #key_tokens,
215                    vec![
216                    #(#lines),*
217                    ]
218                )
219            );
220            false
221        }
222    };
223
224    expanded.into()
225}
226
227/// Execute a set of conditional expressions based on user-selected choices.
228///
229/// Each arm has the form `Choice => Expr`. If a choice was selected, its
230/// corresponding expression (the RHS) is executed (executions occur in order), regardless of whether
231/// the choice's key (the LHS) is currently visible.
232///
233/// Each LHS key is a [`ifengine::elements::ChoiceVariant`], dictating its visibility.
234/// Any type that implements `Into<`[`Line`](ifengine::view::Line)`>` will coerce to `Choice::Once`.
235/// Any `Option<Into<Line>>` will coerce to `Choice::None` or `Choice::Always`.
236///
237/// The return type is a [bool; n] representing which of the options were hidden (NOT displayed).
238///
239/// The following example permits you to pass once you have chosen 2 party members and checked the special event.
240/// ```rust
241///  if mchoice! {
242///     s.c1.name.is_empty().then_some(link!("member_1_choice_1", _oracle_1)),
243///     s.c1.name.is_empty().then_some(link!("member_1_choice_2", _oracle_2)),
244///     s.c2.name.is_empty().then_some(link!("member_2_choice_1", _walker_1)),
245///     s.c2.name.is_empty().then_some(link!("member_2_choice_2", _walker_2)),
246///     (!s.part1.seen.contains("special_event")).then_some(link!("special_event", _interpreter_2))
247///  }.all() {
248///     GOTO!(p6)
249///  }
250/// ```
251
252#[proc_macro]
253pub fn mchoice(input: TokenStream) -> TokenStream {
254    let ChoiceInput { maybe_key, arms } = syn::parse_macro_input!(input as ChoiceInput);
255
256    let key = maybe_key.into_tokens();
257
258    let arm_blocks: Vec<_> = arms
259        .iter()
260        .enumerate()
261        .map(|(i, LineArm { line, block })| {
262            let i = i as u8;
263
264            let block_tokens = match block {
265                Some(b) => quote! { #b },
266                None => quote! {},
267            };
268
269            quote! {
270                if (__ifengine_tmp_mask & (1u64 << #i)) != 0 {
271                    #block_tokens
272                }
273                if let Some(l) = ifengine::elements::ChoiceVariant::from(#line)
274                .as_line((__ifengine_tmp_mask & (1u64 << #i)) != 0)
275                {
276                    __ifengine_tmp_lines.push((#i, l));
277                    __ifengine_visible_mask[#i as usize] = false;
278                }
279            }
280        })
281        .collect::<Vec<_>>();
282
283    let n = arms.len();
284
285    let expanded = quote! {
286        {
287            let __ifengine_tmp_mask = __ifengine_page_state.get(#key).unwrap_or(0u64);
288            let mut __ifengine_tmp_lines = Vec::new();
289            let mut __ifengine_visible_mask = [true; #n];
290
291            #(#arm_blocks)*
292
293            if ! __ifengine_tmp_lines.is_empty() {
294                __ifengine_page_state.push(
295                    ifengine::view::Object::Choice(#key, __ifengine_tmp_lines)
296                );
297            }
298
299            __ifengine_visible_mask
300        }
301    };
302
303    expanded.into()
304}
305
306/// Executes code for a set of selectable choices. Prefer to use [`dchoice`] for brevity.
307///
308/// # Overview
309/// This macro displays list of choices, and registers a corresponding handler
310/// for each selection. The handler is specified as a `match` expression, where
311/// each arm corresponds to a choice and contains the code to execute when
312/// that choice is selected. Unlike the other choice elements ([`choice`], [`mchoice`]),
313/// the conditional expression is evaluated only the first time it's choice is selected.
314/// The intent is that the arms are used to set values for the user's custom [`ifengine::core::GameContext`].
315///
316/// # Arguments
317/// - [`MaybeKey`] (Optional)
318/// - **Choices list**: A `Vec<(Id, Line)>` representing the selectable options. The Id can either be a [#repr(u8)] Unit Enum or a pure u8.
319/// - **Handler**: A `match` statement handling each choice.
320///
321/// # Match statement
322/// The match token of the match statement should be given with your custom enum type, or not given if you identify your choices with pure u8's.
323///
324/// # Additional
325/// A [`MaybeKey`] can be specified in the first argument:
326///   When a choice is clicked, it sets the value of its key to its id (cast as a u8) in [`PageState`].
327///   When the page is next rendered, this value is removed, and the corresponding match arm is run.
328///   It is discouraged to specify this: by default, it will be automatically generated.
329///
330/// # Example
331/// ```rust
332/// let choices = vec![
333///     (0, line!("A")),
334///     (1, line!("B")),
335///     (2, line!("C")),
336/// ];
337///
338/// if let Some(x) = dynamic_choice!(choices) {
339///     match x {
340///         0 => "A clicked",
341///         1 => "B clicked",
342///         2 => "C clicked",
343///     }
344/// }
345/// ```
346///
347/// It is also possible to use unit enums:
348/// ```rust
349/// #[derive(Clone, Copy)]
350/// #[repr(u8)]
351/// enum DChoices { A, B, C }
352///
353/// let choices = vec![
354///     (DChoices::A, line!("A")),
355///     (DChoices::B, line!("B")),
356///     (DChoices::C, line!("C")),
357/// ];
358///
359/// if let Some(x) = dynamic_choice!(choices) {
360///     match x {
361///         DChoices::A => "A clicked",
362///         DChoices::B => "B clicked",
363///         DChoices::C => "C clicked",
364///     }
365/// }
366/// ```
367
368#[proc_macro]
369pub fn dynamic_choice(input: TokenStream) -> TokenStream {
370    let KeyExpr { maybe_key, expr } = syn::parse_macro_input!(input as KeyExpr);
371    let key_tokens = maybe_key.into_tokens();
372
373    let expanded = quote! {
374        {
375            // Push the DynamicChoice object
376            __ifengine_page_state.push(ifengine::view::Object::Choice(
377                #key_tokens,
378                #expr
379                .into_iter()
380                .map(|(t, l)| (t as u8, ifengine::view::Line::from(l)))
381                .collect()
382            ));
383
384            __ifengine_page_state.remove_mask_last(#key_tokens).map(|x|
385                unsafe { std::mem::transmute::<u8, _>(x) }
386            )
387        }
388    };
389
390    expanded.into()
391}
392
393struct DChoicesInput {
394    pub maybe_key: MaybeKey,
395    pub expr: Expr,
396    pub arms: Vec<Arm>,
397}
398
399impl Parse for DChoicesInput {
400    fn parse(input: ParseStream) -> Result<Self> {
401        let KeyExpr { maybe_key, expr } = input.parse()?;
402
403        if !input.is_empty() {
404            input.parse::<Token![,]>()?;
405        }
406
407        let mut arms = Vec::new();
408        while !input.is_empty() {
409            arms.push(input.parse::<Arm>()?);
410        }
411
412        Ok(DChoicesInput {
413            maybe_key,
414            expr,
415            arms,
416        })
417    }
418}
419
420/// A version of [`dynamic_choice`] with slightly abbreviated syntax.
421/// It can be a bit trickier to use this if your list is fully dynamic,
422/// but the flexibility of match statements should be sufficient for any purpose.
423/// For example, if you have pairs of (handler, choice), you can simply them and use
424/// `c => handlers[c]` as your arm.
425///
426/// # Example
427/// ```rust
428/// let choices = vec![
429///     line!("A"),
430///     line!("B"),
431///     line!("C"),
432/// ];
433/// dchoice! { choices,
434///     0 => "A clicked",
435///     1 => "B clicked",
436///     2 => "C clicked",
437/// }
438/// ```
439#[proc_macro]
440pub fn dchoice(input: TokenStream) -> TokenStream {
441    let DChoicesInput {
442        maybe_key,
443        expr,
444        arms,
445    } = parse_macro_input!(input as DChoicesInput);
446
447    let key_tokens = maybe_key.into_tokens();
448
449    let has_wildcard = arms.iter().any(|arm| matches!(arm.pat, syn::Pat::Wild(_)));
450    let catch_all = if has_wildcard {
451        quote! {}
452    } else {
453        quote! { _ => {} }
454    };
455    let match_block = if arms.is_empty() {
456        quote! {}
457    } else {
458        quote! {
459            if let Some(__ifengine_id) = __ifengine_page_state.remove_mask_last(#key_tokens) {
460                match __ifengine_id as usize {
461                    #(#arms)*
462                    #catch_all
463                }
464            }
465        }
466    };
467
468    let expanded = quote! {
469        {
470            __ifengine_page_state.push(ifengine::view::Object::Choice(
471                #key_tokens,
472                #expr
473                .iter()
474                .enumerate()
475                .map(|(i, l)| (i as u8, ifengine::view::Line::from(l.clone())))
476                .collect()
477            ));
478
479            if let Some(__ifengine_id) = __ifengine_page_state.remove_mask_last(#key_tokens) {
480                #match_block
481            }
482        }
483    };
484
485    expanded.into()
486}
487/// Create a paragraph with interactive elements from a string.
488///
489/// Interactive text sections are automatically added from text delimited by [[ and ]] (Also see: [`mparagraph`]).
490/// The return type is the value of whichever text token that was clicked.
491///
492/// # Syntax
493/// ```text
494/// dparagraph!(maybe_key, expr1, expr2, ..., exprN)
495///
496/// # Additional
497/// Text is trimmed
498/// Multiple inputs are accepted, and produce multiple paragraphs
499/// The interactive elements must not change between renders.
500#[proc_macro]
501pub fn dparagraph(input: TokenStream) -> TokenStream {
502    let KeyExprs { maybe_key, exprs } = syn::parse_macro_input!(input as KeyExprs);
503
504    let key = maybe_key.into_tokens();
505
506    let expanded = quote! {{
507        let mut ret = None;
508
509        #(
510            let mut __ifengine_tmp_strings =
511            ifengine::utils::split_braced(&ifengine::utils::trim_lines(&#exprs));
512
513            if let Some(__ifengine_tmp_val) = __ifengine_page_state
514            .remove(#key)
515            .and_then(|k| {
516                ifengine::utils::find_hash_match(__ifengine_tmp_strings.iter().step_by(2), k).cloned()
517            }) {
518                ret = Some(__ifengine_tmp_val);
519            }
520
521            __ifengine_page_state.push(
522                ifengine::view::Object::Paragraph(
523                    ifengine::view::Line::from_interleaved_actions::<false>(
524                        (__ifengine_page_state.id(), #key),
525                        __ifengine_tmp_strings
526                    )
527                )
528            );
529        )*
530
531        ret
532    }};
533
534    expanded.into()
535}
536
537/// Create a paragraph with interactive elements from a string.
538///
539/// Interactive text sections are automatically added from text delimited by [[ and ]] (Also see: [`dparagraph`]).
540/// This macro tracks and returns which of the interactive elements had been clicked since page load as a `Vec<bool>`.
541///
542/// # Note
543/// The interactive elements must not change between renders.
544
545#[proc_macro]
546pub fn mparagraph(input: TokenStream) -> TokenStream {
547    let KeyExpr { maybe_key, expr } = syn::parse_macro_input!(input as KeyExpr);
548
549    let key = maybe_key.into_tokens();
550
551    let expanded = quote! {{
552        let strings =
553        ifengine::utils::split_braced(&ifengine::utils::trim_lines(&#expr));
554        let count = strings.len() / 2;
555
556        __ifengine_page_state.push(
557            ifengine::view::Object::Paragraph(
558                ifengine::view::Line::from_interleaved_actions::<true>(
559                    (__ifengine_page_state.id(), #key),
560                    strings
561                )
562            )
563        );
564
565        __ifengine_page_state.get_mask::<64>(#key)[..count].to_vec()
566    }};
567
568    expanded.into()
569}
570
571// ----------------- ELEMENTS -------------------
572
573/// Push a (Object)[ifengine::view::Object] to the current (View)[ifengine::View]
574#[proc_macro]
575pub fn push(input: TokenStream) -> TokenStream {
576    let expr = parse_macro_input!(input as Expr);
577
578    let expanded = quote! {
579        __ifengine_page_state.push(
580            #expr
581        );
582    };
583
584    expanded.into()
585}
586
587struct LineArgs {
588    exprs: Vec<Expr>,
589    trailer: Option<LitStr>,
590}
591
592impl syn::parse::Parse for LineArgs {
593    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
594        let mut exprs = Vec::new();
595        let mut trailer = None;
596
597        while !input.is_empty() {
598            if input.peek(Token![::]) {
599                let _coloncolon: Token![::] = input.parse()?;
600                let lit: LitStr = input.parse()?;
601                trailer = Some(lit);
602                break;
603            }
604
605            exprs.push(input.parse()?);
606
607            if input.peek(Token![,]) {
608                let _ = input.parse::<Token![,]>()?;
609            } else {
610                break;
611            }
612        }
613
614        Ok(LineArgs { exprs, trailer })
615    }
616}
617
618/// Pure text element, constructed from a sequence of spans.
619///
620/// # Additional
621/// A trailing [`ifengine::view::RenderData`] can be specified following `::`.
622///
623/// # Example
624/// ```rust
625/// text!("Hello, world!");
626/// text!("Hello, ", "world!" :: "my_render_data");
627/// ```
628#[proc_macro]
629pub fn text(input: TokenStream) -> TokenStream {
630    let LineArgs { exprs, trailer } = syn::parse_macro_input!(input as LineArgs);
631
632    let string_expr = match trailer {
633        Some(s) => quote!(#s),
634        None => quote!(""),
635    };
636
637    let expanded = quote! {
638        __ifengine_page_state.push(
639            ifengine::view::Object::Text(
640                ifengine::view::Line::from_spans(
641                    vec![#(#exprs.into()),*]
642                ),
643                #string_expr
644            )
645        );
646    };
647
648    TokenStream::from(expanded)
649}
650
651/// A sequence of text elements. See [`text`].
652///
653/// # Additional
654/// A trailing [`ifengine::view::RenderData`] can be specified following `::`.
655///
656/// Note that text and choice styling may differ depending on the renderer, particularly with respect to vertical item spacing.
657/// When you want to display choices without handling their effects seperately from actions attached to their spans, prefer [`dchoice`].
658///
659/// # Example
660/// ```rust
661/// texts!("Line 1", "Line 2");
662/// ```
663#[proc_macro]
664pub fn texts(input: TokenStream) -> TokenStream {
665    let LineArgs { exprs, trailer } = syn::parse_macro_input!(input as LineArgs);
666
667    let string_expr = match trailer {
668        Some(s) => quote!(#s),
669        None => quote!(""),
670    };
671
672    let expanded = quote! {
673        #(
674            __ifengine_page_state.push(
675                ifengine::view::Object::Text(
676                    ifengine::view::Line::from(#exprs),
677                    #string_expr
678                )
679            );
680        )*
681    };
682
683    TokenStream::from(expanded)
684}
685
686/// Create a paragraph from a sequence of spans.
687///
688/// # Example
689/// ```rust
690/// paragraph!(span1, span2, span3);
691/// ```
692#[proc_macro]
693pub fn paragraph(input: TokenStream) -> TokenStream {
694    let exprs_parsed = parse_macro_input!(input with Punctuated<Expr, Token![,]>::parse_terminated);
695    let exprs: Vec<Expr> = exprs_parsed.into_iter().collect();
696
697    let expanded = quote! {
698        __ifengine_page_state.push(
699            ifengine::view::Object::Paragraph(
700                ifengine::view::Line::from_spans(vec![#(ifengine::view::Span::from_lingual(#exprs)),*])
701            )
702        );
703    };
704
705    TokenStream::from(expanded)
706}
707
708/// Shorthand for creating multiple paragraphs from a sequence of [`crate::view::Line`]'s.
709///
710/// # Example
711/// ```rust
712/// paragraphs!(line1, line2, line3);
713/// ```
714///
715/// # Additional
716/// Any type implementing `Into<Line>` is accepted.
717#[proc_macro]
718pub fn paragraphs(input: TokenStream) -> TokenStream {
719    use quote::quote;
720    use syn::punctuated::Punctuated;
721    use syn::{Expr, Token, parse_macro_input};
722
723    let exprs_parsed = parse_macro_input!(input with Punctuated<Expr, Token![,]>::parse_terminated);
724    let exprs: Vec<Expr> = exprs_parsed.into_iter().collect();
725
726    let expanded = quote! {
727        #(
728            __ifengine_page_state.push(
729                ifengine::view::Object::Paragraph(
730                    ifengine::view::Line::from_lingual(#exprs)
731                )
732            );
733        )*
734    };
735
736    TokenStream::from(expanded)
737}
738
739/// Push an image from a string literal.
740///
741/// # Example
742/// ```rust
743/// img!("assets/logo.png");
744/// img!("https://example.com/logo.png", (100, 50));
745/// ```
746#[proc_macro]
747pub fn img(input: TokenStream) -> TokenStream {
748    use quote::quote;
749    use syn::punctuated::Punctuated;
750    use syn::{Expr, Lit, Token, parse_macro_input};
751
752    let exprs_parsed = parse_macro_input!(input with Punctuated<Expr, Token![,]>::parse_terminated);
753    let exprs: Vec<&Expr> = exprs_parsed.iter().collect();
754
755    let (path_expr, size_expr) = match exprs.len() {
756        1 => (exprs[0], None),
757        2 => (exprs[0], Some(exprs[1])),
758        _ => {
759            return syn::Error::new_spanned(exprs_parsed, "image! macro expects 1 or 2 arguments")
760                .to_compile_error()
761                .into();
762        }
763    };
764
765    let image_tokens = if let Expr::Lit(lit) = path_expr
766        && let Lit::Str(s) = &lit.lit
767    {
768        let path = s.value();
769        if path.starts_with("http://") || path.starts_with("https://") {
770            if let Some(size) = size_expr {
771                quote! { ifengine::view::Image::new_url(#path).with_size(#size) }
772            } else {
773                quote! { ifengine::view::Image::new_url(#path) }
774            }
775        } else {
776            if let Some(size) = size_expr {
777                quote! { ifengine::view::Image::new_local(#path, include_bytes!(#path)).with_size(#size) }
778            } else {
779                quote! { ifengine::view::Image::new_local(#path, include_bytes!(#path)) }
780            }
781        }
782    } else {
783        return syn::Error::new_spanned(path_expr, "expected string literal")
784            .to_compile_error()
785            .into();
786    };
787
788    let expanded = quote! {
789        __ifengine_page_state.push(ifengine::view::Object::Image(#image_tokens));
790    };
791
792    TokenStream::from(expanded)
793}
794
795/// Markdown heading.
796///
797/// # Example
798/// ```rust
799/// h!("Title", 2)
800/// ```
801#[proc_macro]
802pub fn h(input: TokenStream) -> TokenStream {
803    let exprs_parsed = parse_macro_input!(input with Punctuated<Expr, Token![,]>::parse_terminated);
804    let exprs: Vec<&Expr> = exprs_parsed.iter().collect();
805
806    if exprs.len() != 2 {
807        return syn::Error::new_spanned(
808            exprs_parsed,
809            "macro expects exactly 2 arguments: text and level",
810        )
811        .to_compile_error()
812        .into();
813    }
814
815    let text = exprs[0];
816    let level = exprs[1];
817
818    let expanded = quote! {
819        __ifengine_page_state.push(
820            ifengine::view::Object::Heading(ifengine::view::Span::from_lingual(#text), #level)
821        );
822    };
823
824    TokenStream::from(expanded)
825}
826
827/// Horizontal rule (`<hr/>`).
828///
829/// # Example
830/// ```rust
831/// hr!()
832/// ```
833#[proc_macro]
834pub fn hr(_input: TokenStream) -> TokenStream {
835    let expanded = quote! {
836        __ifengine_page_state.push(ifengine::view::Object::Break);
837    };
838
839    TokenStream::from(expanded)
840}
841
842// --------------- ALTS -------------------------
843
844#[derive(Clone)]
845enum AltVariant {
846    Stop,
847    Shuffle,
848    Cycle,
849}
850
851impl Default for AltVariant {
852    fn default() -> Self {
853        AltVariant::Stop
854    }
855}
856
857impl Parse for AltVariant {
858    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
859        let ident: syn::Ident = input.parse()?;
860        match ident.to_string().as_str() {
861            "Stop" => Ok(AltVariant::Stop),
862            "Shuffle" => Ok(AltVariant::Shuffle),
863            "Cycle" => Ok(AltVariant::Cycle),
864            _ => Err(syn::Error::new(
865                ident.span(),
866                "expected AltVariant: Stop | Shuffle | Cycle",
867            )),
868        }
869    }
870}
871
872struct AltsInput {
873    maybe_key: MaybeKey,
874    list: Vec<Expr>,
875    variant: Option<AltVariant>,
876}
877
878impl Parse for AltsInput {
879    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
880        let content;
881        syn::bracketed!(content in input);
882
883        let maybe_key = input.parse()?;
884
885        let mut list = Vec::new();
886        while !content.is_empty() {
887            list.push(content.parse()?);
888            if content.peek(Token![,]) {
889                let _: Token![,] = content.parse()?;
890            }
891        }
892
893        // optional variant
894        let variant = if !input.is_empty() {
895            input.parse::<Token![,]>()?;
896            Some(input.parse()?)
897        } else {
898            None
899        };
900
901        Ok(Self {
902            maybe_key,
903            list,
904            variant,
905        })
906    }
907}
908
909/// Cycle between multiple alternative spans on click.
910///
911/// ## Behavior
912///
913/// - The first span is shown when no prior state exists.
914/// - The active index is stored in page state under the provided key.
915///
916/// ## Variants
917///
918/// ### `stop` (default)
919/// Advances until the last span, then stops:
920///
921/// ```text
922/// A → B → C (stops)
923/// ```
924///
925/// ### `cycle`
926/// Advances to the next span on activation, wrapping around:
927///
928/// ```text
929/// A → B → C → A → …
930/// ```
931///
932/// ### `shuffle`
933/// Chooses a random span each time, avoiding immediate repetition.
934/// The internal state uses the low bit as a regeneration flag.
935///
936/// ## Syntax
937///
938/// ```ignore
939/// alts!(key?, variant?, [expr, expr, ...])
940/// ```
941///
942/// - `key` (optional): Explicit state key
943/// - `variant` (optional): `cycle`, `stop`, or `shuffle`
944/// - `expr`: Any value convertible into a `Span`
945///
946/// ## Examples
947///
948/// Basic cycling:
949///
950/// ```ignore
951/// alts!([
952///     "Look around",
953///     "Open the door",
954///     "Wait",
955/// ])
956/// ```
957///
958/// With an explicit key and variant:
959///
960/// ```ignore
961/// alts!(
962///     (5),
963///     shuffle,
964///     [
965///         "Attack",
966///         "Defend",
967///         "Flee",
968///     ]
969/// )
970/// ```
971///
972/// ## Notes
973///
974/// - State is updated via [`ifengine::Action::Inc`] or [`ifengine::Action::Set`].
975/// - Random selection uses the page state's RNG.
976/// - The macro expands to an expression producing a `Span`.
977/// - Shuffle and Cycle are hidden during simulation.
978#[proc_macro]
979pub fn alts(input: TokenStream) -> TokenStream {
980    let AltsInput {
981        maybe_key,
982        list,
983        variant,
984    } = parse_macro_input!(input as AltsInput);
985
986    let key = maybe_key.into_tokens();
987
988    let variant = variant.unwrap_or_default();
989    let list_init = quote! { &[ #(#list),* ] };
990
991    let expanded = match variant {
992        AltVariant::Stop => {
993            quote! {{
994                let alts = #list_init;
995
996                if let Some(idx) = __ifengine_page_state.get(#key) {
997                    ifengine::view::Span::from(
998                        alts[(idx as usize + 1).min(alts.len() - 1)]
999                    )
1000                    .with_action(ifengine::Action::Inc((__ifengine_page_state.id(), #key)))
1001                } else {
1002                    ifengine::view::Span::from(
1003                        alts[0]
1004                    )
1005                    .with_action(ifengine::Action::Inc((__ifengine_page_state.id(), #key)))
1006                }
1007            }}
1008        }
1009
1010        AltVariant::Shuffle => {
1011            quote! {{
1012                let alts = #list_init;
1013
1014                // Determine tmp index
1015                let idx = if let Some(prev) = __ifengine_page_state.get(#key) {
1016                    if prev & 1 == 0 {
1017                        (prev as usize) >> 1
1018                    } else {
1019                        // regenerate, excluding previous index
1020                        let new_idx = __ifengine_page_state.rand(alts.len(), &[(prev as usize) >> 1]);
1021                        __ifengine_page_state.insert(#key, (new_idx as u64) << 1);
1022                        new_idx
1023                    }
1024                } else {
1025                    let new_idx = __ifengine_page_state.rand(alts.len(), &[]);
1026                    __ifengine_page_state.insert(#key, (new_idx as u64) << 1);
1027                    new_idx
1028                } ;
1029
1030                // Use it and store back with last bit set
1031                ifengine::view::Span::from(alts[idx])
1032                .with_action(ifengine::Action::Set(
1033                    (__ifengine_page_state.id(), #key),
1034                    ((idx as u64) << 1) + 1
1035                ))
1036                .no_sim()
1037            }}
1038        }
1039
1040        AltVariant::Cycle => {
1041            quote! {{
1042                let alts = #list_init;
1043
1044                if let Some(idx) = __ifengine_page_state.get(#key) {
1045                    ifengine::view::Span::from(
1046                        alts[(idx as usize) % alts.len()]
1047                    )
1048                    .with_action(ifengine::Action::Inc((__ifengine_page_state.id(), #key)))
1049                    .no_sim()
1050                } else {
1051                    ifengine::view::Span::from(
1052                        alts[0]
1053                    )
1054                    .with_action(ifengine::Action::Inc((__ifengine_page_state.id(), #key)))
1055                    .no_sim()
1056                }
1057            }}
1058        }
1059    };
1060
1061    expanded.into()
1062}
1063//
1064
1065// ------------- SPANS/CLOSURES ----------------------
1066
1067struct CountInput {
1068    maybe_key: MaybeKey,
1069    closure: ExprClosure,
1070}
1071
1072impl Parse for CountInput {
1073    fn parse(input: ParseStream) -> Result<Self> {
1074        let maybe_key = input.parse()?;
1075        let closure = input.parse()?;
1076
1077        Ok(CountInput { maybe_key, closure })
1078    }
1079}
1080
1081/// Use a closure to compute a span based on how many times the span has been clicked.
1082///
1083/// # Syntax
1084/// ```rust
1085/// let span_count = read_key!(6); // Can be called before
1086/// let span = count!((6), |val| "span");
1087/// ```
1088///
1089/// # Arguments
1090/// - [`MaybeKey`]
1091/// - `closure`: A closure taking the current value and returning a `Span`.
1092#[proc_macro]
1093pub fn count(input: TokenStream) -> TokenStream {
1094    let CountInput { maybe_key, closure } = syn::parse_macro_input!(input as CountInput);
1095    let key = maybe_key.into_tokens();
1096
1097    let expanded = quote! {{
1098        ifengine::view::Span::from(
1099            (#closure)(__ifengine_page_state.get(#key).unwrap_or_default())
1100        )
1101        .with_action(ifengine::Action::Inc((__ifengine_page_state.id(), #key)))
1102        .no_sim()
1103    }};
1104
1105    expanded.into()
1106}
1107
1108struct ClickInput {
1109    maybe_key: MaybeKey,
1110    expr: Expr,
1111    block: Expr,
1112}
1113
1114impl Parse for ClickInput {
1115    fn parse(input: ParseStream) -> Result<Self> {
1116        let maybe_key = input.parse()?;
1117
1118        let expr: Expr = input.parse()?;
1119
1120        let block = if input.peek(Token![,]) {
1121            input.parse::<Token![,]>()?;
1122            input.parse::<Expr>()?
1123        } else {
1124            syn::parse_quote!({})
1125        };
1126
1127        Ok(ClickInput {
1128            maybe_key,
1129            expr,
1130            block,
1131        })
1132    }
1133}
1134
1135/// Run code on click.
1136///
1137/// # Syntax
1138/// ```rust
1139/// p!(click!(span, expr ))
1140/// ```
1141///
1142/// # Arguments
1143/// - [`MaybeKey`]
1144/// - `span`: The element to display. The link style is automatically applied.
1145/// - `block`: Executed exactly once whenever the key is clicked.
1146///
1147/// # Note
1148/// The handler is evaluated before the span is.
1149#[proc_macro]
1150pub fn click(input: TokenStream) -> TokenStream {
1151    let ClickInput {
1152        maybe_key,
1153        expr,
1154        block,
1155    } = syn::parse_macro_input!(input as ClickInput);
1156    let key = maybe_key.into_tokens();
1157
1158    let expanded = quote! {{
1159        if __ifengine_page_state.was_zero(#key) {
1160            let _ = #block;
1161        };
1162
1163        let span = ifengine::view::Span::from(
1164            #expr
1165        )
1166        .with_action(ifengine::Action::Inc((__ifengine_page_state.id(), #key)))
1167        .as_link();
1168
1169        // sim the handler once
1170        if __ifengine_page_state.get(#key).is_some() {
1171            span.no_sim()
1172        } else {
1173            span
1174        }
1175    }};
1176
1177    expanded.into()
1178}
1179
1180/// Run a function only once when the page is first loaded.
1181///
1182/// # Syntax
1183/// ```rust
1184/// fresh!(|| { /* code */ })
1185/// ```
1186#[proc_macro]
1187pub fn fresh(input: TokenStream) -> TokenStream {
1188    let closure = parse_macro_input!(input as ExprClosure);
1189
1190    let expanded = quote! {{
1191        if __ifengine_page_state.fresh() {
1192            (#closure)();
1193        }
1194    }};
1195
1196    expanded.into()
1197}
1198
1199// -------------- SPANS -------------------------
1200
1201/// Create a link [`Span`] that navigates backward.
1202///
1203/// - `$e`: Display text.
1204/// - `$n`: Optional number of steps to go back (defaults to 1).
1205///
1206/// # Additional
1207/// This option will be hidden during simulation if no number is specified
1208#[proc_macro]
1209pub fn back(input: TokenStream) -> TokenStream {
1210    let ExprAndOptional { expr, n } = parse_macro_input!(input as ExprAndOptional);
1211
1212    let expanded = if let Some(n_expr) = n {
1213        quote! {
1214            ifengine::view::Span::from(#expr)
1215            .as_link()
1216            .with_action(ifengine::Action::Back(#n_expr))
1217        }
1218    } else {
1219        quote! {
1220            ifengine::view::Span::from(#expr)
1221            .as_link()
1222            .with_action(ifengine::Action::Back(1))
1223            .no_sim()
1224        }
1225    };
1226
1227    TokenStream::from(expanded)
1228}
1229
1230/// Immediately yield a [`Response::View`] with the current [`View`].
1231///
1232/// This returns `!`, exiting the current function.
1233#[proc_macro]
1234#[allow(non_snake_case)]
1235pub fn r#YIELD(_input: TokenStream) -> TokenStream {
1236    let expanded = quote! {
1237        return __ifengine_page_state.into_response()
1238    };
1239    expanded.into()
1240}
1241
1242// ------------ KEY OPERATIONS -------------------
1243/// Read the value of a key of the internal [`PageState`]
1244///
1245/// Elements push to the view in the order they are called.
1246/// This can be used to query their state out of order.
1247///
1248/// Beware that the implementation details of the internal page state that these keys index is internal and should not be relied on!
1249///
1250/// # Example
1251/// ```rust
1252/// let value = read_key!(my_key);
1253/// ```
1254#[proc_macro]
1255pub fn read_key(input: TokenStream) -> TokenStream {
1256    let expr = syn::parse_macro_input!(input as syn::Expr);
1257
1258    let expanded = quote! {
1259        __ifengine_page_state.get(#expr)
1260    };
1261
1262    expanded.into()
1263}
1264
1265/// Read a key as a bitmask. See [`read_key`].
1266///
1267/// # Example
1268/// ```rust
1269/// let mask = read_key_mask!(my_key); // [bool; 64]
1270/// let mask = read_key_mask!(my_key, 5); // [bool; 5]
1271/// ```
1272#[proc_macro]
1273pub fn read_key_mask(input: TokenStream) -> TokenStream {
1274    let ExprAndOptional { expr: key, n } = syn::parse_macro_input!(input as ExprAndOptional);
1275
1276    let n = n.unwrap_or_else(|| syn::parse_quote!(64));
1277
1278    quote! {
1279        __ifengine_page_state.get_mask::<#n>(#key)
1280    }
1281    .into()
1282}
1283
1284/// Set a key to a value. See [`read_key`].
1285///
1286/// # Example
1287/// ```rust
1288/// set_key!(my_key, 42);
1289/// ```
1290#[proc_macro]
1291pub fn set_key(input: TokenStream) -> TokenStream {
1292    let expr = syn::parse_macro_input!(input as syn::Expr);
1293
1294    let expanded = quote! {
1295        __ifengine_page_state.insert(#expr.0, #expr.1)
1296    };
1297
1298    expanded.into()
1299}
1300
1301/// Set individual bits of a key to true. See [`read_key_mask`].
1302///
1303/// # Example
1304/// ```rust
1305/// set_key_mask!(my_key, 0, 2, 4);
1306/// ```
1307#[proc_macro]
1308pub fn set_key_mask(input: TokenStream) -> TokenStream {
1309    use syn::{Expr, Token, parse::Parser, punctuated::Punctuated};
1310
1311    let parts = match Punctuated::<Expr, Token![,]>::parse_terminated.parse(input) {
1312        Ok(parts) => parts,
1313        Err(e) => return e.to_compile_error().into(),
1314    };
1315
1316    let mut iter = parts.iter();
1317    let key = if let Some(key) = iter.next() {
1318        key
1319    } else {
1320        return syn::Error::new_spanned(parts, "expected key")
1321            .to_compile_error()
1322            .into();
1323    };
1324    let bits: Vec<&Expr> = iter.collect();
1325
1326    let mut mask = 0u64;
1327    for expr in &bits {
1328        if let Expr::Lit(syn::ExprLit {
1329            lit: syn::Lit::Int(i),
1330            ..
1331        }) = expr
1332        {
1333            match i.base10_parse::<usize>() {
1334                Ok(bit) => mask |= 1u64 << bit,
1335                Err(_) => {
1336                    return syn::Error::new_spanned(i, "failed to parse bit position")
1337                        .to_compile_error()
1338                        .into();
1339                }
1340            }
1341        } else {
1342            return syn::Error::new_spanned(expr, "bit positions must be integer literals")
1343                .to_compile_error()
1344                .into();
1345        }
1346    }
1347
1348    let expanded = quote! {
1349        {
1350            let old = __ifengine_page_state.get(#key).unwrap_or(0u64);
1351            __ifengine_page_state.insert(#key, old | #mask);
1352        }
1353    };
1354
1355    expanded.into()
1356}
1357
1358/// Clear individual bits of a key. See [`read_key_mask`].
1359///
1360/// # Example
1361/// ```rust
1362/// unset_key_mask!(my_key, 1, 3);
1363/// ```
1364#[proc_macro]
1365pub fn unset_key_mask(input: TokenStream) -> TokenStream {
1366    use syn::{Expr, Token, parse::Parser, punctuated::Punctuated};
1367
1368    let parts = match Punctuated::<Expr, Token![,]>::parse_terminated.parse(input) {
1369        Ok(parts) => parts,
1370        Err(e) => return e.to_compile_error().into(),
1371    };
1372
1373    let mut iter = parts.iter();
1374    let key = if let Some(key) = iter.next() {
1375        key
1376    } else {
1377        return syn::Error::new_spanned(parts, "expected key")
1378            .to_compile_error()
1379            .into();
1380    };
1381    let bits: Vec<&Expr> = iter.collect();
1382
1383    let mut mask = 0u64;
1384    for expr in &bits {
1385        if let Expr::Lit(syn::ExprLit {
1386            lit: syn::Lit::Int(i),
1387            ..
1388        }) = expr
1389        {
1390            match i.base10_parse::<usize>() {
1391                Ok(bit) => mask |= 1u64 << bit,
1392                Err(_) => {
1393                    return syn::Error::new_spanned(i, "failed to parse bit position")
1394                        .to_compile_error()
1395                        .into();
1396                }
1397            }
1398        } else {
1399            return syn::Error::new_spanned(expr, "bit positions must be integer literals")
1400                .to_compile_error()
1401                .into();
1402        }
1403    }
1404
1405    let expanded = quote! {
1406        {
1407            let old = __ifengine_page_state.get(#key).unwrap_or(0u64);
1408            __ifengine_page_state.insert(#key, old & !#mask);
1409        }
1410    };
1411
1412    expanded.into()
1413}
1414
1415/// Increment the value of a key. See [`read_key`].
1416///
1417/// # Example
1418/// ```rust
1419/// inc_key!(my_key);
1420/// ```
1421#[proc_macro]
1422pub fn inc_key(input: TokenStream) -> TokenStream {
1423    let expr = syn::parse_macro_input!(input as syn::Expr);
1424
1425    let expanded = quote! {
1426        {
1427            let k = #expr;
1428            let v = __ifengine_page_state.get(k).unwrap_or(0);
1429            __ifengine_page_state.insert(k, v.wrapping_add(1));
1430        }
1431    };
1432
1433    expanded.into()
1434}
1435
1436/// Reset (remove) a key from state. See [`read_key`].
1437///
1438/// # Example
1439/// ```rust
1440/// reset_key!(my_key);
1441/// ```
1442#[proc_macro]
1443pub fn reset_key(input: TokenStream) -> TokenStream {
1444    let expr = syn::parse_macro_input!(input as syn::Expr);
1445
1446    let expanded = quote! {
1447        __ifengine_page_state.remove(#expr)
1448    };
1449
1450    expanded.into()
1451}
1452
1453// ------------ TAGS ------------------
1454
1455// note: this doesn't work
1456/// [Tags](crate::core::GameTags) the current page.
1457///
1458/// Pass `Sticky` to persist the tag between pages.
1459///
1460/// # Examples
1461///
1462/// ```rust
1463/// tag!(my_value);          // non-sticky tag
1464/// tag!(my_value, Sticky);  // sticky tag
1465/// tag!(my_value, Once);    // apply only once
1466/// ```
1467#[proc_macro]
1468pub fn tag(input: TokenStream) -> TokenStream {
1469    use quote::quote;
1470    use syn::parse::{Parse, ParseStream, Result};
1471    use syn::{Expr, Ident, Token, parse_macro_input};
1472
1473    struct TagInput {
1474        expr: Expr,
1475        mode: Option<Ident>,
1476    }
1477
1478    impl Parse for TagInput {
1479        fn parse(input: ParseStream) -> Result<Self> {
1480            let expr: Expr = input.parse()?;
1481            let mode: Option<Ident> = if input.peek(Token![,]) {
1482                input.parse::<Token![,]>()?;
1483                Some(input.parse()?)
1484            } else if !input.is_empty() {
1485                Some(input.parse()?)
1486            } else {
1487                None
1488            };
1489            Ok(TagInput { expr, mode })
1490        }
1491    }
1492
1493    let TagInput { expr, mode } = parse_macro_input!(input as TagInput);
1494
1495    let sticky = match mode {
1496        Some(id) => match id.to_string().as_str() {
1497            "Sticky" => true,
1498            "Once" => false,
1499            _ => {
1500                return syn::Error::new_spanned(&id, "Expected `Sticky` or `Once`")
1501                    .to_compile_error()
1502                    .into();
1503            }
1504        },
1505        None => false,
1506    };
1507
1508    let expanded = quote! {
1509        __ifengine_page_state.tag(#expr, #sticky)
1510    };
1511
1512    expanded.into()
1513}
1514
1515/// Removes a [tag](`crate::core::GameTags`)
1516#[proc_macro]
1517pub fn untag(input: TokenStream) -> TokenStream {
1518    let expr = syn::parse_macro_input!(input as syn::Expr);
1519
1520    let expanded = quote! {
1521        __ifengine_page_state.untag(#expr)
1522    };
1523
1524    expanded.into()
1525}
1526
1527/// Returns whether the current function is running in a [`crate::run::Simulation`].
1528#[proc_macro]
1529pub fn in_sim(_: TokenStream) -> TokenStream {
1530    let expanded = quote! {
1531        __ifengine_page_state.simulating
1532    };
1533
1534    expanded.into()
1535}
1536
1537// ------------ UTILS ------------------
1538
1539/// Debug display the current [`PageState`]
1540#[proc_macro]
1541pub fn page_dbg(_input: TokenStream) -> TokenStream {
1542    let expanded = quote! {
1543        // #[cfg(debug_assertions)]
1544        dbg!(&__ifengine_page_state)
1545    };
1546    expanded.into()
1547}
1548
1549/// Debug display the current view
1550#[proc_macro]
1551pub fn view_dbg(_input: TokenStream) -> TokenStream {
1552    let expanded = quote! {
1553        dbg!(&__ifengine_page_state.view)
1554    };
1555    expanded.into()
1556}