Skip to main content

cheers_ast/
lib.rs

1pub mod basics;
2pub mod component;
3pub mod control;
4pub mod generate;
5mod syntax;
6
7use std::marker::PhantomData;
8
9pub use basics::UnquotedName;
10use proc_macro2::{Span, TokenStream};
11use quote::{ToTokens, quote};
12use syn::{
13    Error, Expr, Ident, LitBool, LitChar, LitFloat, LitInt, LitStr, Token, braced, bracketed,
14    ext::IdentExt,
15    parenthesized,
16    parse::{Parse, ParseStream},
17    punctuated::Punctuated,
18    token::{Brace, Bracket, Paren},
19};
20
21use self::{
22    basics::Literal,
23    component::Component,
24    control::Control,
25    generate::{
26        AnyBlock, AttributeNameCheck, AttributeNameCheckKind, ElementCheck, ElementKind, Generate,
27        Generator, NodeFlavour,
28    },
29};
30use crate::generate::Context;
31
32pub type Document = Nodes<ElementNode>;
33
34/// Syntactic staticness for Cheers markup.
35///
36/// Static nodes can be rendered without evaluating caller-provided Rust expressions. This is a
37/// conservative syntax-only property. Any Rust expression, component, or control-flow node is
38/// considered dynamic.
39pub trait SyntaxStatic {
40    fn is_static(&self) -> bool;
41}
42
43pub struct DatastarSourceNodes(pub Nodes<AttributeValueNode>);
44
45pub struct ScriptSourceNodes(pub Nodes<AttributeValueNode>);
46
47impl Parse for DatastarSourceNodes {
48    fn parse(input: ParseStream) -> syn::Result<Self> {
49        input.parse().map(Self)
50    }
51}
52
53impl SyntaxStatic for DatastarSourceNodes {
54    fn is_static(&self) -> bool {
55        self.0.is_static()
56    }
57}
58
59impl Generate for DatastarSourceNodes {
60    const CONTEXT: Context = Context::DatastarSource;
61
62    fn generate(&mut self, g: &mut Generator<'_>) {
63        g.with_context_override(Context::DatastarSource, |g| {
64            if self.0.0.iter().any(Node::is_control) {
65                g.push_in_block(Brace::default(), |g| g.push_all(&mut self.0.0));
66            } else {
67                g.push_all(&mut self.0.0);
68            }
69        });
70    }
71}
72
73impl Parse for ScriptSourceNodes {
74    fn parse(input: ParseStream) -> syn::Result<Self> {
75        input.parse().map(Self)
76    }
77}
78
79impl SyntaxStatic for ScriptSourceNodes {
80    fn is_static(&self) -> bool {
81        self.0.is_static()
82    }
83}
84
85impl Generate for ScriptSourceNodes {
86    const CONTEXT: Context = Context::ScriptSource;
87
88    fn generate(&mut self, g: &mut Generator<'_>) {
89        g.with_context_override(Context::ScriptSource, |g| {
90            if self.0.0.iter().any(Node::is_control) {
91                g.push_in_block(Brace::default(), |g| g.push_all(&mut self.0.0));
92            } else {
93                g.push_all(&mut self.0.0);
94            }
95        });
96    }
97}
98
99pub trait Node: Generate {
100    fn is_control(&self) -> bool;
101}
102
103#[allow(clippy::large_enum_variant)]
104pub enum ElementNode {
105    Element(Element),
106    Component(Component),
107    Literal(Literal),
108    Control(Control<Self>),
109    Expr(ParenExpr<Self>),
110    Group(Group<Self>),
111}
112
113impl Node for ElementNode {
114    fn is_control(&self) -> bool {
115        matches!(self, Self::Control(_))
116    }
117}
118
119impl SyntaxStatic for ElementNode {
120    fn is_static(&self) -> bool {
121        match self {
122            Self::Element(element) => element.is_static(),
123            Self::Literal(_) => true,
124            Self::Group(group) => group.is_static(),
125            Self::Component(_) | Self::Control(_) | Self::Expr(_) => false,
126        }
127    }
128}
129
130impl Generate for ElementNode {
131    const CONTEXT: Context = Context::Element;
132
133    fn generate(&mut self, g: &mut Generator<'_>) {
134        match self {
135            Self::Element(element) => g.push(element),
136            Self::Component(component) => g.push(component),
137            Self::Literal(lit) => g.push_escaped_literal(Self::CONTEXT, &lit.lit_str()),
138            Self::Control(control) => g.push(control),
139            Self::Expr(expr) => g.push(expr),
140            Self::Group(group) => g.push(group),
141        }
142    }
143}
144
145pub struct ParenExpr<N: Node> {
146    pub paren_token: Paren,
147    pub mode: ParenExprMode,
148    pub body: ParenExprBody,
149    phantom: PhantomData<N>,
150}
151
152#[allow(clippy::large_enum_variant)]
153pub enum ParenExprBody {
154    Unit,
155    Expr(Expr),
156    Tuple(Punctuated<Expr, Token![,]>),
157}
158
159impl ToTokens for ParenExprBody {
160    fn to_tokens(&self, tokens: &mut TokenStream) {
161        match self {
162            Self::Unit => {}
163            Self::Expr(expr) => expr.to_tokens(tokens),
164            Self::Tuple(elems) => elems.to_tokens(tokens),
165        }
166    }
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum ParenExprMode {
171    Normal,
172    Ref,
173}
174
175impl ParenExprMode {
176    pub const fn is_ref(self) -> bool {
177        matches!(self, Self::Ref)
178    }
179
180    pub const fn prefix_len(self) -> usize {
181        if self.is_ref() { "@&".len() } else { 0 }
182    }
183
184    fn validate_ref_expr(expr: &Expr) -> syn::Result<()> {
185        fn is_supported(expr: &Expr) -> bool {
186            match expr {
187                Expr::Path(_) => true,
188                Expr::Field(field) => is_supported(&field.base),
189                Expr::Paren(paren) => is_supported(&paren.expr),
190                _ => false,
191            }
192        }
193
194        if is_supported(expr) {
195            Ok(())
196        } else {
197            Err(Error::new_spanned(expr, "unsupported borrow expression"))
198        }
199    }
200
201    fn parse_prefix(input: ParseStream) -> syn::Result<Self> {
202        if input.peek(Token![@]) {
203            input.parse::<Token![@]>()?;
204            input.parse::<Token![&]>()?;
205            Ok(Self::Ref)
206        } else {
207            Ok(Self::Normal)
208        }
209    }
210
211    fn parse_expr(input: ParseStream) -> syn::Result<(Self, ParenExprBody)> {
212        let mode = Self::parse_prefix(input)?;
213
214        if input.is_empty() {
215            if mode.is_ref() {
216                return Err(Error::new(input.span(), "expected expression after `@&`"));
217            }
218
219            return Ok((mode, ParenExprBody::Unit));
220        }
221
222        let expr: Expr = input.parse()?;
223        let body = if input.peek(Token![,]) {
224            let mut elems = Punctuated::new();
225            elems.push_value(expr);
226
227            while input.peek(Token![,]) {
228                elems.push_punct(input.parse()?);
229
230                if input.is_empty() {
231                    break;
232                }
233
234                elems.push_value(input.parse()?);
235            }
236
237            ParenExprBody::Tuple(elems)
238        } else {
239            ParenExprBody::Expr(expr)
240        };
241
242        if !input.is_empty() {
243            return Err(input.error("unexpected tokens after expression"));
244        }
245
246        if mode.is_ref() {
247            match &body {
248                ParenExprBody::Expr(expr) => {
249                    Self::validate_ref_expr(expr).map_err(|err| {
250                        Error::new(
251                            err.span(),
252                            "`(@&...)` only supports simple path and field expressions",
253                        )
254                    })?;
255                }
256                ParenExprBody::Unit | ParenExprBody::Tuple(_) => {
257                    return Err(Error::new_spanned(
258                        &body,
259                        "`(@&...)` only supports simple path and field expressions",
260                    ));
261                }
262            }
263        }
264
265        Ok((mode, body))
266    }
267}
268
269pub struct BorrowExpr<E> {
270    pub paren_token: Option<Paren>,
271    pub mode: ParenExprMode,
272    pub expr: E,
273}
274
275pub type DataExpr = BorrowExpr<Expr>;
276
277impl Parse for BorrowExpr<Expr> {
278    fn parse(input: ParseStream) -> syn::Result<Self> {
279        let (paren_token, mode, expr) = if input.peek(Paren) {
280            let content;
281            let paren_token = parenthesized!(content in input);
282            let mode = ParenExprMode::parse_prefix(&content)?;
283            let expr: Expr = content.parse()?;
284
285            (Some(paren_token), mode, expr)
286        } else {
287            (None, ParenExprMode::Normal, input.parse()?)
288        };
289
290        if mode.is_ref() {
291            ParenExprMode::validate_ref_expr(&expr).map_err(|err| {
292                Error::new(
293                    err.span(),
294                    "`(@&...)` only supports simple path and field expressions",
295                )
296            })?;
297        }
298
299        Ok(Self {
300            paren_token,
301            mode,
302            expr,
303        })
304    }
305}
306
307impl<E: ToTokens> ToTokens for BorrowExpr<E> {
308    fn to_tokens(&self, tokens: &mut TokenStream) {
309        let write = |tokens: &mut TokenStream| {
310            if self.mode.is_ref() {
311                quote!(@&).to_tokens(tokens);
312            }
313            self.expr.to_tokens(tokens);
314        };
315
316        if let Some(paren_token) = self.paren_token {
317            paren_token.surround(tokens, write);
318        } else {
319            write(tokens);
320        }
321    }
322}
323
324impl<N: Node> Parse for ParenExpr<N> {
325    fn parse(input: ParseStream) -> syn::Result<Self> {
326        let content;
327        let paren_token = parenthesized!(content in input);
328        let (mode, body) = ParenExprMode::parse_expr(&content)?;
329
330        Ok(Self {
331            paren_token,
332            mode,
333            body,
334            phantom: PhantomData,
335        })
336    }
337}
338
339impl<N: Node> Generate for ParenExpr<N> {
340    const CONTEXT: Context = N::CONTEXT;
341
342    fn generate(&mut self, g: &mut Generator<'_>) {
343        match self.mode {
344            ParenExprMode::Normal => g.push_expr(self.paren_token, Self::CONTEXT, &self.body),
345            ParenExprMode::Ref => g.push_ref_expr(self.paren_token, Self::CONTEXT, &self.body),
346        }
347    }
348}
349
350impl<N: Node> ToTokens for ParenExpr<N> {
351    fn to_tokens(&self, tokens: &mut TokenStream) {
352        self.paren_token.surround(tokens, |tokens| {
353            if self.mode.is_ref() {
354                quote!(@&).to_tokens(tokens);
355            }
356            self.body.to_tokens(tokens);
357        });
358    }
359}
360
361pub struct Group<N: Node> {
362    pub brace_token: Brace,
363    pub nodes: Nodes<N>,
364}
365
366impl<N: Node + SyntaxStatic> SyntaxStatic for Group<N> {
367    fn is_static(&self) -> bool {
368        self.nodes.is_static()
369    }
370}
371
372impl Parse for Group<AttributeValueNode> {
373    fn parse(input: ParseStream) -> syn::Result<Self> {
374        let content;
375        let brace_token = braced!(content in input);
376
377        Ok(Self {
378            brace_token,
379            nodes: content.parse()?,
380        })
381    }
382}
383
384impl<N: Node> Generate for Group<N> {
385    const CONTEXT: Context = N::CONTEXT;
386
387    fn generate(&mut self, g: &mut Generator<'_>) {
388        g.push(&mut self.nodes);
389    }
390}
391
392pub struct Nodes<N: Node>(pub Vec<N>);
393
394impl<N: Node + SyntaxStatic> SyntaxStatic for Nodes<N> {
395    fn is_static(&self) -> bool {
396        self.0.iter().all(SyntaxStatic::is_static)
397    }
398}
399
400impl<N: Node> Nodes<N> {
401    fn block(&mut self, g: &mut Generator<'_>, brace_token: Brace) -> AnyBlock {
402        g.block_with(
403            brace_token,
404            |g| {
405                g.push_all(&mut self.0);
406            },
407            true,
408        )
409    }
410}
411
412impl<N: Node + Parse> Parse for Nodes<N> {
413    fn parse(input: ParseStream) -> syn::Result<Self> {
414        Ok(Self({
415            let mut nodes = Vec::new();
416
417            while !input.is_empty() {
418                nodes.push(input.parse()?);
419            }
420
421            nodes
422        }))
423    }
424}
425
426impl<N: Node> Generate for Nodes<N> {
427    const CONTEXT: Context = N::CONTEXT;
428
429    fn generate(&mut self, g: &mut Generator<'_>) {
430        if self.0.iter().any(Node::is_control) {
431            g.push_in_block(Brace::default(), |g| g.push_all(&mut self.0));
432        } else {
433            g.push_all(&mut self.0);
434        }
435    }
436}
437
438pub struct Element {
439    pub name: UnquotedName,
440    pub attrs: Vec<Attribute>,
441    pub body: ElementBody,
442}
443
444impl SyntaxStatic for Element {
445    fn is_static(&self) -> bool {
446        self.attrs.iter().all(SyntaxStatic::is_static) && self.body.is_static()
447    }
448}
449
450impl Generate for Element {
451    const CONTEXT: Context = Context::Element;
452
453    fn generate(&mut self, g: &mut Generator<'_>) {
454        let flavour = g.node_flavour();
455        let module = flavour.elements_module();
456        let mut el_checks = ElementCheck::new(&self.name, self.body.kind(flavour), module);
457
458        g.push_str("<");
459        g.push_literal(self.name.lit());
460        #[cfg(feature = "pi-extension")]
461        {
462            if !self.has_regular_attribute("data-cheers-source") {
463                let span = self.name.span();
464                let start = span.start();
465                g.push_element_source_hint(LitStr::new(
466                    &format!("{}:{}:{}", span.file(), start.line, start.column + 1),
467                    span,
468                ));
469            }
470        }
471
472        for attr in &mut self.attrs {
473            g.push(&mut *attr);
474            if let Some(check) = attr.check() {
475                el_checks.push_attribute(check);
476            }
477        }
478
479        match &mut self.body {
480            ElementBody::Normal { children, .. } => {
481                g.push_str(">");
482
483                let child_flavour = flavour.child_flavour(&self.name);
484                if child_flavour != flavour {
485                    g.push_with_flavour(child_flavour, |g| g.push(children));
486                } else {
487                    g.push(children);
488                }
489
490                g.push_str("</");
491                g.push_literal(self.name.lit());
492                g.push_str(">");
493            }
494            ElementBody::Void { .. } => g.push_str(flavour.void_close()),
495        }
496
497        g.record_element(el_checks);
498    }
499}
500
501impl Element {
502    #[cfg(feature = "pi-extension")]
503    fn has_regular_attribute(&self, name: &str) -> bool {
504        self.attrs.iter().any(|attr| {
505            matches!(
506                attr,
507                Attribute::Regular { name: attr_name, .. }
508                if attr_name.literals().into_iter().map(|l| l.value()).collect::<String>() == name
509            )
510        })
511    }
512}
513
514pub enum ElementBody {
515    Normal {
516        brace_token: Brace,
517        children: Nodes<ElementNode>,
518    },
519    Void {
520        semi_token: Token![;],
521    },
522}
523
524impl SyntaxStatic for ElementBody {
525    fn is_static(&self) -> bool {
526        match self {
527            Self::Normal { children, .. } => children.is_static(),
528            Self::Void { .. } => true,
529        }
530    }
531}
532
533impl ElementBody {
534    const fn kind(&self, flavour: NodeFlavour) -> ElementKind {
535        flavour.element_kind(matches!(self, Self::Void { .. }))
536    }
537}
538
539#[allow(clippy::large_enum_variant)]
540pub enum Attribute {
541    Regular {
542        name: AttributeName,
543        kind: AttributeKind,
544    },
545    Data {
546        bang_token: Token![!],
547        data: Data,
548    },
549}
550
551impl SyntaxStatic for Attribute {
552    fn is_static(&self) -> bool {
553        match self {
554            Self::Regular { kind, .. } => kind.is_static(),
555            Self::Data { data, .. } => data.is_static(),
556        }
557    }
558}
559
560impl Attribute {
561    fn check(&self) -> Option<AttributeNameCheck> {
562        match &self {
563            Attribute::Regular { name, .. } => name.check(false),
564            Attribute::Data { data, .. } => match (&data.namespace, data.name.ident()) {
565                (Some(namespace), Some(name)) => {
566                    let mut check = AttributeNameCheck::new(
567                        AttributeNameCheckKind::Namespace(namespace.clone()),
568                        name.clone(),
569                        true,
570                    );
571                    check.push_data_modifiers(data.modifiers.as_ref());
572                    Some(check)
573                }
574                (None, Some(name)) => {
575                    let mut check =
576                        AttributeNameCheck::new(AttributeNameCheckKind::Normal, name.clone(), true);
577                    check.push_data_modifiers(data.modifiers.as_ref());
578                    Some(check)
579                }
580                _ => None,
581            },
582        }
583    }
584}
585
586impl Parse for Attribute {
587    fn parse(input: ParseStream) -> syn::Result<Self> {
588        if let Some(bang_token) = input.parse::<Option<Token![!]>>()? {
589            Ok(Self::Data {
590                bang_token,
591                data: input.parse()?,
592            })
593        } else {
594            let name = input.parse::<AttributeName>()?;
595            let kind = if input.peek(Token![=]) {
596                input.parse::<Token![=]>()?;
597                if let Some(toggle) = input.call(Toggle::parse_optional)? {
598                    AttributeKind::Option(toggle)
599                } else {
600                    AttributeKind::Value {
601                        value: input.parse()?,
602                        toggle: input.call(Toggle::parse_optional)?,
603                    }
604                }
605            } else {
606                AttributeKind::Empty(input.call(Toggle::parse_optional)?)
607            };
608
609            Ok(Self::Regular { name, kind })
610        }
611    }
612}
613
614impl Generate for Attribute {
615    const CONTEXT: Context = Context::AttributeValue;
616
617    fn generate(&mut self, g: &mut Generator<'_>) {
618        match self {
619            Attribute::Regular { name, kind } => match kind {
620                AttributeKind::Value { value, toggle, .. } => {
621                    if let Some(toggle) = toggle {
622                        g.push_conditional(toggle.parenthesized(), |g| {
623                            g.push_str(" ");
624                            g.push_literals(name.literals());
625                            g.push_str("=\"");
626                            g.push(value);
627                            g.push_str("\"");
628                        });
629                    } else {
630                        g.push_str(" ");
631                        g.push_literals(name.literals());
632                        g.push_str("=\"");
633                        g.push(value);
634                        g.push_str("\"");
635                    }
636                }
637                AttributeKind::Option(option) => {
638                    let option_expr = &option.expr;
639
640                    let value = Ident::new("value", Span::mixed_site());
641
642                    g.push_conditional(
643                        quote!(let ::core::option::Option::Some(#value) = (#option_expr)),
644                        |g| {
645                            g.push_str(" ");
646                            g.push_literals(name.literals());
647                            g.push_str("=\"");
648                            g.push_expr(Paren::default(), Self::CONTEXT, &value);
649                            g.push_str("\"");
650                        },
651                    );
652                }
653                AttributeKind::Empty(Some(toggle)) => {
654                    g.push_conditional(toggle.parenthesized(), |g| {
655                        g.push_str(" ");
656                        g.push_literals(name.literals());
657                    });
658                }
659                AttributeKind::Empty(None) => {
660                    g.push_str(" ");
661                    g.push_literals(name.literals());
662                }
663            },
664            Attribute::Data { data, .. } => g.push(data),
665        }
666    }
667}
668
669#[derive(Clone)]
670pub enum AttributeName {
671    Namespace {
672        namespace: UnquotedName,
673        rest: UnquotedName,
674    },
675    Normal {
676        name: UnquotedName,
677    },
678    Unchecked(LitStr),
679}
680
681impl AttributeName {
682    fn check(&self, data: bool) -> Option<AttributeNameCheck> {
683        match self {
684            Self::Unchecked(_) => None,
685            Self::Namespace { namespace, rest } => Some(AttributeNameCheck::new(
686                AttributeNameCheckKind::Namespace(namespace.clone()),
687                rest.clone(),
688                data,
689            )),
690            Self::Normal { name } => Some(AttributeNameCheck::new(
691                AttributeNameCheckKind::Normal,
692                name.clone(),
693                data,
694            )),
695        }
696    }
697
698    fn literals(&self) -> Vec<LitStr> {
699        match self {
700            Self::Namespace { namespace, rest } => {
701                let mut literals = vec![namespace.lit()];
702                let separator = if namespace == &"xml" || namespace == &"xmlns" {
703                    ":"
704                } else {
705                    "-"
706                };
707                literals.push(LitStr::new(separator, namespace.span()));
708                literals.push(rest.lit());
709                literals
710            }
711            Self::Normal { name, .. } => vec![name.lit()],
712            Self::Unchecked(lit) => vec![lit.clone()],
713        }
714    }
715}
716
717impl Parse for AttributeName {
718    fn parse(input: ParseStream) -> syn::Result<Self> {
719        let lookahead = input.lookahead1();
720
721        if lookahead.peek(Ident::peek_any) || lookahead.peek(LitInt) {
722            let name = input.parse()?;
723            if input.peek(Token![:]) {
724                input.parse::<Token![:]>()?;
725                Ok(Self::Namespace {
726                    namespace: name,
727                    rest: input.parse()?,
728                })
729            } else {
730                Ok(Self::Normal { name })
731            }
732        } else if lookahead.peek(LitStr) {
733            let s = input.parse::<LitStr>()?;
734            let value = s.value();
735
736            for c in value.chars() {
737                if c.is_whitespace() {
738                    return Err(Error::new_spanned(
739                        &s,
740                        "Attribute names cannot contain whitespace",
741                    ));
742                } else if c.is_control() {
743                    return Err(Error::new_spanned(
744                        &s,
745                        "Attribute names cannot contain control characters",
746                    ));
747                } else if c == '>' || c == '/' || c == '=' {
748                    return Err(Error::new_spanned(
749                        &s,
750                        format!("Attribute names cannot contain '{c}' characters"),
751                    ));
752                } else if c == '"' || c == '\'' {
753                    return Err(Error::new_spanned(
754                        &s,
755                        "Attribute names cannot contain quotes",
756                    ));
757                }
758            }
759
760            Ok(Self::Unchecked(s))
761        } else {
762            Err(lookahead.error())
763        }
764    }
765}
766
767#[allow(clippy::large_enum_variant)]
768pub enum AttributeKind {
769    Value {
770        value: AttributeValueNode,
771        toggle: Option<Toggle>,
772    },
773    Empty(Option<Toggle>),
774    Option(Toggle),
775}
776
777impl SyntaxStatic for AttributeKind {
778    fn is_static(&self) -> bool {
779        match self {
780            Self::Value {
781                value,
782                toggle: None,
783            } => value.is_static(),
784            Self::Empty(None) => true,
785            Self::Value {
786                toggle: Some(_), ..
787            }
788            | Self::Empty(Some(_))
789            | Self::Option(_) => false,
790        }
791    }
792}
793
794#[allow(clippy::large_enum_variant)]
795pub enum AttributeValueNode {
796    Literal(Literal),
797    Group(Group<Self>),
798    Control(Control<Self>),
799    Expr(ParenExpr<Self>),
800    Ident(Ident),
801}
802
803impl Node for AttributeValueNode {
804    fn is_control(&self) -> bool {
805        matches!(self, Self::Control(_))
806    }
807}
808
809impl SyntaxStatic for AttributeValueNode {
810    fn is_static(&self) -> bool {
811        match self {
812            Self::Literal(_) => true,
813            Self::Group(group) => group.is_static(),
814            Self::Control(_) | Self::Expr(_) | Self::Ident(_) => false,
815        }
816    }
817}
818
819impl Parse for AttributeValueNode {
820    fn parse(input: ParseStream) -> syn::Result<Self> {
821        let lookahead = input.lookahead1();
822
823        if lookahead.peek(LitStr)
824            || lookahead.peek(LitInt)
825            || lookahead.peek(LitBool)
826            || lookahead.peek(LitFloat)
827            || lookahead.peek(LitChar)
828        {
829            input.parse().map(Self::Literal)
830        } else if lookahead.peek(Brace) {
831            input.parse().map(Self::Group)
832        } else if lookahead.peek(Token![@]) {
833            input.parse().map(Self::Control)
834        } else if lookahead.peek(Paren) {
835            input.parse().map(Self::Expr)
836        } else if lookahead.peek(Ident) {
837            input.parse().map(Self::Ident)
838        } else {
839            Err(lookahead.error())
840        }
841    }
842}
843
844impl Generate for AttributeValueNode {
845    const CONTEXT: Context = Context::AttributeValue;
846
847    fn generate(&mut self, g: &mut Generator<'_>) {
848        match self {
849            Self::Literal(lit) => g.push_escaped_literal(Self::CONTEXT, &lit.lit_str()),
850            Self::Group(group) => g.push(group),
851            Self::Control(control) => g.push(control),
852            Self::Expr(paren_expr) => g.push(paren_expr),
853            Self::Ident(ident) => {
854                g.push_expr(Paren::default(), Self::CONTEXT, ident);
855            }
856        }
857    }
858}
859
860pub struct Toggle {
861    pub bracket_token: Bracket,
862    pub expr: Expr,
863}
864
865impl Toggle {
866    fn parenthesized(&self) -> TokenStream {
867        let paren_token = Paren {
868            span: self.bracket_token.span,
869        };
870
871        let mut tokens = TokenStream::new();
872
873        paren_token.surround(&mut tokens, |tokens| {
874            self.expr.to_tokens(tokens);
875        });
876
877        quote! {
878            {
879                #[allow(unused_parens)]
880                #tokens
881            }
882        }
883    }
884
885    fn parse_optional(input: ParseStream) -> syn::Result<Option<Self>> {
886        if input.peek(Bracket) {
887            input.parse().map(Some)
888        } else {
889            Ok(None)
890        }
891    }
892}
893
894impl Parse for Toggle {
895    fn parse(input: ParseStream) -> syn::Result<Self> {
896        let content;
897
898        Ok(Self {
899            bracket_token: bracketed!(content in input),
900            expr: content.parse()?,
901        })
902    }
903}
904
905impl BorrowExpr<Expr> {
906    fn paren_token(&self) -> Paren {
907        self.paren_token.unwrap_or_default()
908    }
909
910    fn borrowed_expr(&self, g: &mut Generator<'_>) -> proc_macro2::TokenStream {
911        match self.mode {
912            ParenExprMode::Normal => {
913                let expr = &self.expr;
914                quote!(&#expr)
915            }
916            ParenExprMode::Ref => {
917                let ref_ident = g.hoist_ref_expr(Paren::default(), &self.expr);
918                quote!(#ref_ident)
919            }
920        }
921    }
922}
923
924pub struct DataExprValue<V: Parse> {
925    pub ident: DataExpr,
926    pub value: V,
927}
928
929impl<V: Parse> Parse for DataExprValue<V> {
930    fn parse(input: ParseStream) -> syn::Result<Self> {
931        Ok(Self {
932            ident: input.parse()?,
933            value: {
934                input.parse::<Token![:]>()?;
935                input.parse()?
936            },
937        })
938    }
939}
940
941#[derive(Clone)]
942pub enum DataName {
943    Present(UnquotedName),
944    Missing(Span),
945}
946
947impl DataName {
948    pub fn ident(&self) -> Option<&UnquotedName> {
949        match self {
950            Self::Present(name) => Some(name),
951            Self::Missing(_) => None,
952        }
953    }
954
955    pub fn lit(&self) -> Option<LitStr> {
956        self.ident().map(UnquotedName::lit)
957    }
958
959    pub fn span(&self) -> Span {
960        match self {
961            Self::Present(name) => name.span(),
962            Self::Missing(span) => *span,
963        }
964    }
965}
966
967pub enum DataModifierPart {
968    Ident(UnquotedName),
969    Literal(Literal),
970}
971
972impl DataModifierPart {
973    fn lit(&self) -> LitStr {
974        match self {
975            Self::Ident(ident) => ident.lit(),
976            Self::Literal(literal) => literal.lit_str(),
977        }
978    }
979
980    fn span(&self) -> Span {
981        match self {
982            Self::Ident(ident) => ident.span(),
983            Self::Literal(literal) => literal.lit_str().span(),
984        }
985    }
986
987    fn validate(&self) -> syn::Result<()> {
988        let lit = self.lit();
989        let value = lit.value();
990
991        if value.is_empty() {
992            return Err(Error::new(
993                self.span(),
994                "Datastar modifier parts cannot be empty",
995            ));
996        }
997
998        for c in value.chars() {
999            if c.is_whitespace() {
1000                return Err(Error::new(
1001                    self.span(),
1002                    "Datastar modifier parts cannot contain whitespace",
1003                ));
1004            } else if c.is_control() {
1005                return Err(Error::new(
1006                    self.span(),
1007                    "Datastar modifier parts cannot contain control characters",
1008                ));
1009            } else if c == '>' || c == '/' || c == '=' || c == '.' {
1010                return Err(Error::new(
1011                    self.span(),
1012                    format!("Datastar modifier parts cannot contain '{c}' characters"),
1013                ));
1014            } else if c == '"' || c == '\'' {
1015                return Err(Error::new(
1016                    self.span(),
1017                    "Datastar modifier parts cannot contain quotes",
1018                ));
1019            }
1020        }
1021
1022        Ok(())
1023    }
1024}
1025
1026impl Parse for DataModifierPart {
1027    fn parse(input: ParseStream) -> syn::Result<Self> {
1028        let lookahead = input.lookahead1();
1029
1030        let part = if lookahead.peek(Ident::peek_any) {
1031            Self::Ident(input.parse()?)
1032        } else if lookahead.peek(LitStr)
1033            || lookahead.peek(LitInt)
1034            || lookahead.peek(LitBool)
1035            || lookahead.peek(LitFloat)
1036            || lookahead.peek(LitChar)
1037        {
1038            Self::Literal(input.parse()?)
1039        } else {
1040            return Err(lookahead.error());
1041        };
1042
1043        part.validate()?;
1044        Ok(part)
1045    }
1046}
1047
1048pub struct DataModifier {
1049    pub name: DataModifierPart,
1050    pub paren_token: Option<Paren>,
1051    pub tags: Punctuated<DataModifierPart, Token![,]>,
1052}
1053
1054impl Parse for DataModifier {
1055    fn parse(input: ParseStream) -> syn::Result<Self> {
1056        let name = input.parse()?;
1057
1058        if input.peek(Paren) {
1059            let content;
1060            let paren_token = parenthesized!(content in input);
1061            Ok(Self {
1062                name,
1063                paren_token: Some(paren_token),
1064                tags: Punctuated::parse_terminated(&content)?,
1065            })
1066        } else {
1067            Ok(Self {
1068                name,
1069                paren_token: None,
1070                tags: Punctuated::new(),
1071            })
1072        }
1073    }
1074}
1075
1076pub struct DataModifiers {
1077    pub bracket_token: Bracket,
1078    pub modifiers: Punctuated<DataModifier, Token![,]>,
1079}
1080
1081impl DataModifiers {
1082    fn literals(&self) -> Vec<LitStr> {
1083        let mut literals = Vec::new();
1084
1085        for modifier in &self.modifiers {
1086            literals.push(LitStr::new("__", modifier.name.span()));
1087            literals.push(modifier.name.lit());
1088
1089            for tag in &modifier.tags {
1090                literals.push(LitStr::new(".", tag.span()));
1091                literals.push(tag.lit());
1092            }
1093        }
1094
1095        literals
1096    }
1097}
1098
1099impl Parse for DataModifiers {
1100    fn parse(input: ParseStream) -> syn::Result<Self> {
1101        let content;
1102
1103        Ok(Self {
1104            bracket_token: bracketed!(content in input),
1105            modifiers: Punctuated::parse_terminated(&content)?,
1106        })
1107    }
1108}
1109
1110#[allow(clippy::large_enum_variant)]
1111pub enum DataContent {
1112    Node(AttributeValueNode),
1113    Signals(Punctuated<DataExprValue<Expr>, Token![,]>),
1114    Kv(Punctuated<DataExprValue<AttributeValueNode>, Token![,]>),
1115    Computed(Punctuated<DataExprValue<AttributeValueNode>, Token![,]>),
1116    Bind(DataExpr),
1117    Empty,
1118    /// Fallback for parsing failures that allows rust-analyzer to emit better completions
1119    Recovered,
1120}
1121
1122impl SyntaxStatic for DataContent {
1123    fn is_static(&self) -> bool {
1124        match self {
1125            Self::Node(node) => node.is_static(),
1126            Self::Empty => true,
1127            Self::Signals(_)
1128            | Self::Kv(_)
1129            | Self::Computed(_)
1130            | Self::Bind(_)
1131            | Self::Recovered => false,
1132        }
1133    }
1134}
1135
1136pub struct Data {
1137    pub namespace: Option<UnquotedName>,
1138    pub name: DataName,
1139    paren_token: Option<Paren>,
1140    pub modifiers: Option<DataModifiers>,
1141    pub content: DataContent,
1142    recovery_error: Option<Error>,
1143}
1144
1145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1146enum DataParseKind {
1147    Node,
1148    Signals,
1149    Kv,
1150    Computed,
1151    Bind,
1152}
1153
1154impl DataParseKind {
1155    fn new(name: Option<&UnquotedName>) -> Self {
1156        match name {
1157            Some(name) if name == &"signals" => Self::Signals,
1158            Some(name) if name == &"style" || name == &"attr" => Self::Kv,
1159            Some(name) if name == &"computed" => Self::Computed,
1160            Some(name) if name == &"indicator" || name == &"bind" => Self::Bind,
1161            _ => Self::Node,
1162        }
1163    }
1164
1165    fn parse_content(self, input: ParseStream) -> syn::Result<DataContent> {
1166        match self {
1167            Self::Signals => Ok(DataContent::Signals(Punctuated::<
1168                DataExprValue<Expr>,
1169                Token![,],
1170            >::parse_terminated(input)?)),
1171            Self::Kv => Ok(DataContent::Kv(Punctuated::<
1172                DataExprValue<AttributeValueNode>,
1173                Token![,],
1174            >::parse_terminated(input)?)),
1175            Self::Computed => Ok(DataContent::Computed(Punctuated::<
1176                DataExprValue<AttributeValueNode>,
1177                Token![,],
1178            >::parse_terminated(
1179                input
1180            )?)),
1181            Self::Bind => Ok(DataContent::Bind(input.parse()?)),
1182            Self::Node => Ok(DataContent::Node(input.parse()?)),
1183        }
1184    }
1185}
1186
1187impl Parse for Data {
1188    fn parse(input: ParseStream) -> syn::Result<Self> {
1189        let mut namespace = None::<UnquotedName>;
1190        let mut recovery_error = None;
1191
1192        if input.peek2(Token![:]) {
1193            namespace = Some(input.parse()?);
1194            input.parse::<Token![:]>()?;
1195        }
1196        let name = match input.parse() {
1197            Ok(name) => DataName::Present(name),
1198            Err(_) => {
1199                let span = namespace
1200                    .as_ref()
1201                    .map(UnquotedName::span)
1202                    .unwrap_or_else(Span::mixed_site);
1203
1204                recovery_error = Some(if let Some(namespace) = &namespace {
1205                    Error::new(
1206                        span,
1207                        format!(
1208                            "expected data attribute name after `{}:`",
1209                            namespace.lit().value()
1210                        ),
1211                    )
1212                } else {
1213                    Error::new(span, "expected data attribute name after `!`")
1214                });
1215
1216                DataName::Missing(span)
1217            }
1218        };
1219
1220        let modifiers = if input.peek(Bracket) {
1221            Some(input.parse()?)
1222        } else {
1223            None
1224        };
1225
1226        if !input.peek(Paren) {
1227            return Ok(Data {
1228                name,
1229                namespace,
1230                paren_token: None,
1231                modifiers,
1232                content: if recovery_error.is_some() {
1233                    DataContent::Recovered
1234                } else {
1235                    DataContent::Empty
1236                },
1237                recovery_error,
1238            });
1239        }
1240
1241        let data;
1242        let paren_token = parenthesized!(data in input);
1243
1244        if recovery_error.is_some() {
1245            return Ok(Self {
1246                namespace,
1247                name,
1248                paren_token: Some(paren_token),
1249                modifiers,
1250                content: DataContent::Recovered,
1251                recovery_error,
1252            });
1253        }
1254
1255        let parse_kind = DataParseKind::new(name.ident());
1256        let content = match parse_kind.parse_content(&data) {
1257            Ok(content) => content,
1258            Err(err) => {
1259                recovery_error = Some(err);
1260                DataContent::Recovered
1261            }
1262        };
1263
1264        Ok(Self {
1265            namespace,
1266            name,
1267            paren_token: Some(paren_token),
1268            modifiers,
1269            content,
1270            recovery_error,
1271        })
1272    }
1273}
1274
1275impl SyntaxStatic for Data {
1276    fn is_static(&self) -> bool {
1277        self.recovery_error.is_none() && self.content.is_static()
1278    }
1279}
1280
1281impl Data {
1282    pub const fn has_parens(&self) -> bool {
1283        self.paren_token.is_some()
1284    }
1285
1286    pub fn paren_span(&self) -> Option<proc_macro2::extra::DelimSpan> {
1287        self.paren_token.map(|paren_token| paren_token.span)
1288    }
1289
1290    fn name_literals(&self) -> Vec<LitStr> {
1291        let mut literals = Vec::new();
1292
1293        if let Some(namespace) = &self.namespace {
1294            literals.push(namespace.lit());
1295            literals.push(LitStr::new(":", namespace.span()));
1296        }
1297
1298        if let Some(name) = self.name.lit() {
1299            let name_str = name.value();
1300            // TODO: I think, we should update everything to use snake_case
1301            let name = LitStr::new(&name_str.replace('_', "-"), name.span());
1302            literals.push(name);
1303        }
1304
1305        if let Some(modifiers) = &self.modifiers {
1306            literals.extend(modifiers.literals());
1307        }
1308
1309        literals
1310    }
1311}
1312
1313impl Generate for Data {
1314    const CONTEXT: Context = Context::AttributeValue;
1315
1316    fn generate(&mut self, g: &mut Generator<'_>) {
1317        if let Some(recovery_error) = &self.recovery_error {
1318            g.push_diagnostic(recovery_error.to_compile_error());
1319        }
1320
1321        let name_literals = self.name_literals();
1322        let has_parens = self.has_parens();
1323
1324        match &mut self.content {
1325            DataContent::Signals(signals) => {
1326                g.push_str(" data-");
1327                g.push_literals(name_literals);
1328                g.push_str("=\"");
1329                g.push_str("{");
1330                let mut first = true;
1331                for d in signals {
1332                    if !first {
1333                        g.push_str(",");
1334                    } else {
1335                        first = false;
1336                    }
1337
1338                    let buffer_ident = Generator::buffer_ident();
1339                    let buffer_expr = quote!(#buffer_ident.as_datastar_buffer());
1340
1341                    let ident_ref = d.ident.borrowed_expr(g);
1342                    let expr = &d.value;
1343                    g.push_stmt(quote! {
1344                        ::cheers::prelude::Signal::__assign(
1345                            #ident_ref,
1346                            #buffer_expr,
1347                            #expr,
1348                        );
1349                    });
1350                }
1351                g.push_str("}");
1352                g.push_str("\"");
1353            }
1354            DataContent::Kv(styles) => {
1355                g.push_str(" data-");
1356                g.push_literals(name_literals);
1357                g.push_str("=\"");
1358                g.push_str("{");
1359                let mut first = true;
1360                for d in styles {
1361                    if !first {
1362                        g.push_str(",");
1363                    } else {
1364                        first = false;
1365                    }
1366
1367                    match d.ident.mode {
1368                        ParenExprMode::Normal => {
1369                            g.push_expr(
1370                                d.ident.paren_token(),
1371                                Context::DatastarSource,
1372                                &d.ident.expr,
1373                            );
1374                        }
1375                        ParenExprMode::Ref => {
1376                            let ident_ref = d.ident.borrowed_expr(g);
1377                            g.push_expr(Paren::default(), Context::DatastarSource, ident_ref);
1378                        }
1379                    }
1380                    g.push_str(":");
1381                    g.push_js_value_node(&mut d.value);
1382                }
1383                g.push_str("}");
1384                g.push_str("\"");
1385            }
1386            DataContent::Computed(d) => {
1387                for d in d {
1388                    g.push_str(" data-");
1389                    g.push_literals(name_literals.clone());
1390                    g.push_str("=\"");
1391                    g.push_str("{");
1392
1393                    let buffer_ident = Generator::buffer_ident();
1394                    let buffer_expr = quote!(#buffer_ident.as_datastar_buffer());
1395                    let ident_ref = d.ident.borrowed_expr(g);
1396                    g.push_stmt(quote! {
1397                        let count = ::cheers::prelude::Signal::__computed_open(
1398                            #ident_ref,
1399                            #buffer_expr
1400                        );
1401                    });
1402                    g.push_js_value_node(&mut d.value);
1403                    g.push_stmt(quote! {
1404                        ::cheers::prelude::Signal::__computed_close(count, #buffer_expr);
1405                    });
1406                    g.push_str("}");
1407                    g.push_str("\"");
1408                }
1409            }
1410            DataContent::Node(attribute_value_node) => {
1411                g.push_str(" data-");
1412                g.push_literals(name_literals);
1413                g.push_str("=\"");
1414                g.push_js_value_node(attribute_value_node);
1415                g.push_str("\"");
1416            }
1417            DataContent::Bind(expr) => {
1418                let expr_ref = expr.borrowed_expr(g);
1419                g.push_str(" data-");
1420                g.push_literals(name_literals);
1421                g.push_str("=\"");
1422                g.push_expr(
1423                    Paren::default(),
1424                    Context::AttributeValue,
1425                    quote! { ::cheers::prelude::Signal::__path(#expr_ref) },
1426                );
1427                g.push_str("\"");
1428            }
1429            DataContent::Empty => {
1430                g.push_str(" data-");
1431                g.push_literals(self.name_literals());
1432            }
1433            DataContent::Recovered => {
1434                g.push_str(" data-");
1435                g.push_literals(name_literals);
1436                if has_parens {
1437                    g.push_str("=\"\"");
1438                }
1439            }
1440        }
1441    }
1442}
1443
1444#[cfg(test)]
1445mod tests {
1446    use syn::parse_str;
1447
1448    use super::{
1449        Attribute, AttributeValueNode, DataContent, DataName, Document, ParenExpr, ParenExprBody,
1450        SyntaxStatic,
1451    };
1452
1453    #[test]
1454    fn syntax_static_accepts_literal_markup() {
1455        let doc = parse_str::<Document>(r#"div class="card" !ignore { "Hello" span { "world" } }"#)
1456            .expect("expected document to parse");
1457
1458        assert!(doc.is_static());
1459    }
1460
1461    #[test]
1462    fn syntax_static_rejects_rust_expressions() {
1463        let doc = parse_str::<Document>(r#"div { (name) }"#).expect("expected document to parse");
1464
1465        assert!(!doc.is_static());
1466    }
1467
1468    #[test]
1469    fn syntax_static_rejects_control_flow() {
1470        let doc = parse_str::<Document>(r#"@if enabled { div { "yes" } }"#)
1471            .expect("expected document to parse");
1472
1473        assert!(!doc.is_static());
1474    }
1475
1476    #[test]
1477    fn syntax_static_rejects_components() {
1478        let doc = parse_str::<Document>(r#"Card { "Hello" }"#).expect("expected document to parse");
1479
1480        assert!(!doc.is_static());
1481    }
1482
1483    #[test]
1484    fn syntax_static_rejects_dynamic_attributes() {
1485        let doc = parse_str::<Document>(r#"button disabled=[is_disabled] { "Save" }"#)
1486            .expect("expected document to parse");
1487
1488        assert!(!doc.is_static());
1489    }
1490
1491    #[test]
1492    fn paren_expr_parses_unit_body_explicitly() {
1493        let expr = parse_str::<ParenExpr<AttributeValueNode>>("()")
1494            .expect("expected unit paren expression to parse");
1495
1496        assert!(matches!(expr.body, ParenExprBody::Unit));
1497    }
1498
1499    #[test]
1500    fn paren_expr_requires_a_single_valid_rust_expression() {
1501        assert!(parse_str::<ParenExpr<AttributeValueNode>>("(foo())").is_ok());
1502        assert!(parse_str::<ParenExpr<AttributeValueNode>>("(foo, bar)").is_ok());
1503        assert!(parse_str::<ParenExpr<AttributeValueNode>>("(foo bar)").is_err());
1504        assert!(parse_str::<ParenExpr<AttributeValueNode>>("(@&)").is_err());
1505    }
1506
1507    #[test]
1508    fn data_attribute_recovers_missing_name_without_placeholder() {
1509        let attr = parse_str::<Attribute>("!").expect("expected attribute to parse");
1510
1511        let Attribute::Data { data, .. } = attr else {
1512            panic!("expected data attribute");
1513        };
1514
1515        assert!(matches!(data.name, DataName::Missing(_)));
1516        assert!(matches!(data.content, DataContent::Recovered));
1517        assert!(data.recovery_error.is_some());
1518        assert!(!data.has_parens());
1519    }
1520
1521    #[test]
1522    fn data_attribute_recovers_invalid_payload() {
1523        let attr = parse_str::<Attribute>("!on:click()").expect("expected attribute to parse");
1524
1525        let Attribute::Data { data, .. } = attr else {
1526            panic!("expected data attribute");
1527        };
1528
1529        assert!(
1530            data.namespace
1531                .as_ref()
1532                .is_some_and(|namespace| namespace == &"on")
1533        );
1534        assert!(matches!(data.name, DataName::Present(ref name) if name == &"click"));
1535        assert!(matches!(data.content, DataContent::Recovered));
1536        assert!(data.recovery_error.is_some());
1537        assert!(data.has_parens());
1538    }
1539
1540    #[test]
1541    fn data_attribute_flags_remain_distinct_from_recovery() {
1542        let attr = parse_str::<Attribute>("!ignore").expect("expected attribute to parse");
1543
1544        let Attribute::Data { data, .. } = attr else {
1545            panic!("expected data attribute");
1546        };
1547
1548        assert!(matches!(data.name, DataName::Present(ref name) if name == &"ignore"));
1549        assert!(matches!(data.content, DataContent::Empty));
1550        assert!(data.recovery_error.is_none());
1551        assert!(!data.has_parens());
1552    }
1553}