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
34pub 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 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 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}