| 59 |
59 |
|
items,
|
| 60 |
60 |
|
} = declaration;
|
| 61 |
61 |
|
|
| 62 |
|
- |
let (shaped, optional) = shaped_name(returns)?;
|
|
62 |
+ |
let shaped = shaped_name(returns)?;
|
| 63 |
63 |
|
|
| 64 |
64 |
|
let docs = docs.iter().map(|line| {
|
| 65 |
65 |
|
let line = LitStr::new(line, Span::call_site());
|
| 71 |
71 |
|
let ty = ¶m.ty;
|
| 72 |
72 |
|
quote!(#name: #ty)
|
| 73 |
73 |
|
});
|
| 74 |
|
- |
let body = value_body(items, &shaped, optional, name.span())?;
|
|
74 |
+ |
let body = value_body(items, &shaped, name.span())?;
|
| 75 |
75 |
|
|
| 76 |
76 |
|
// The vocabulary type, not the path the file happened to import it under:
|
| 77 |
77 |
|
// team.rs takes `Screen as Described` because it has a function called
|
| 78 |
78 |
|
// `screen`, and a declaration names what it returns rather than what the
|
| 79 |
79 |
|
// module around it calls that.
|
| 80 |
|
- |
let shaped_type = format_ident!("{}", shaped, span = returns.span());
|
| 81 |
|
- |
let returns = if optional {
|
| 82 |
|
- |
quote!(::std::option::Option<::quasi_router::#shaped_type>)
|
| 83 |
|
- |
} else {
|
| 84 |
|
- |
quote!(::quasi_router::#shaped_type)
|
|
80 |
+ |
let returns = match &shaped {
|
|
81 |
+ |
Shaped::Nodes => quote!(::std::vec::Vec<::quasi_router::Node>),
|
|
82 |
+ |
Shaped::Single { name, optional } => {
|
|
83 |
+ |
let shaped_type = format_ident!("{}", name, span = returns.span());
|
|
84 |
+ |
if *optional {
|
|
85 |
+ |
quote!(::std::option::Option<::quasi_router::#shaped_type>)
|
|
86 |
+ |
} else {
|
|
87 |
+ |
quote!(::quasi_router::#shaped_type)
|
|
88 |
+ |
}
|
|
89 |
+ |
}
|
| 85 |
90 |
|
};
|
| 86 |
91 |
|
|
| 87 |
92 |
|
Ok(quote! {
|
| 91 |
96 |
|
})
|
| 92 |
97 |
|
}
|
| 93 |
98 |
|
|
| 94 |
|
- |
/// The vocabulary type a shape returns, and whether omission is allowed.
|
| 95 |
|
- |
///
|
| 96 |
|
- |
/// `Option<T>` is the grammar's `single "?"`, and rule R10 is what makes it
|
| 97 |
|
- |
/// usable: a body whose emission is guarded away yields `None`.
|
| 98 |
|
- |
fn shaped_name(returns: &syn::Type) -> Result<(String, bool)> {
|
|
99 |
+ |
/// What a shape returns.
|
|
100 |
+ |
enum Shaped {
|
|
101 |
+ |
/// One vocabulary type, which the body's single emission is. `optional` is
|
|
102 |
+ |
/// the grammar's `single "?"`, and rule R10 is what makes it usable: a body
|
|
103 |
+ |
/// whose emission is guarded away yields `None`.
|
|
104 |
+ |
Single { name: String, optional: bool },
|
|
105 |
+ |
/// `Vec<Node>`: the members of a panel, in order, with nothing wrapping
|
|
106 |
+ |
/// them.
|
|
107 |
+ |
///
|
|
108 |
+ |
/// The one shape that is not a single emission, and it is not a container
|
|
109 |
+ |
/// either. `user_support` demanded it: the dashboard strip draws the region
|
|
110 |
+ |
/// and its `id`, so the panel's own fill must add no second one, and the
|
|
111 |
+ |
/// tab that answers over htmx wraps the same members itself. 52 of the
|
|
112 |
+ |
/// population's 484 shapes return this, third after `Slot` and `Node`.
|
|
113 |
+ |
Nodes,
|
|
114 |
+ |
}
|
|
115 |
+ |
|
|
116 |
+ |
/// The type a shape returns, read off the declaration.
|
|
117 |
+ |
fn shaped_name(returns: &syn::Type) -> Result<Shaped> {
|
| 99 |
118 |
|
let syn::Type::Path(path) = returns else {
|
| 100 |
119 |
|
return Err(syn::Error::new_spanned(
|
| 101 |
120 |
|
returns,
|
| 105 |
124 |
|
let Some(last) = path.path.segments.last() else {
|
| 106 |
125 |
|
return Err(syn::Error::new_spanned(returns, "an empty return type"));
|
| 107 |
126 |
|
};
|
| 108 |
|
- |
if last.ident == "Option" {
|
| 109 |
|
- |
let syn::PathArguments::AngleBracketed(arguments) = &last.arguments else {
|
| 110 |
|
- |
return Err(syn::Error::new_spanned(returns, "`Option` of what?"));
|
|
127 |
+ |
if last.ident == "Vec" {
|
|
128 |
+ |
let Some(inner) = sole_argument(returns, &last.arguments)? else {
|
|
129 |
+ |
return Err(syn::Error::new_spanned(returns, "`Vec` of what?"));
|
| 111 |
130 |
|
};
|
| 112 |
|
- |
let Some(syn::GenericArgument::Type(inner)) = arguments.args.first() else {
|
| 113 |
|
- |
return Err(syn::Error::new_spanned(returns, "`Option` of what?"));
|
| 114 |
|
- |
};
|
| 115 |
|
- |
let (shaped, nested) = shaped_name(inner)?;
|
| 116 |
|
- |
if nested {
|
|
131 |
+ |
if !matches!(shaped_name(inner)?, Shaped::Single { ref name, optional: false } if name == "Node")
|
|
132 |
+ |
{
|
| 117 |
133 |
|
return Err(syn::Error::new_spanned(
|
| 118 |
134 |
|
returns,
|
| 119 |
|
- |
"a shape omits its result or does not; there is no second omission",
|
|
135 |
+ |
"the only list a shape builds is `Vec<Node>`, which is a panel's members",
|
| 120 |
136 |
|
));
|
| 121 |
137 |
|
}
|
| 122 |
|
- |
return Ok((shaped, true));
|
|
138 |
+ |
return Ok(Shaped::Nodes);
|
| 123 |
139 |
|
}
|
| 124 |
|
- |
Ok((last.ident.to_string(), false))
|
|
140 |
+ |
if last.ident == "Option" {
|
|
141 |
+ |
let Some(inner) = sole_argument(returns, &last.arguments)? else {
|
|
142 |
+ |
return Err(syn::Error::new_spanned(returns, "`Option` of what?"));
|
|
143 |
+ |
};
|
|
144 |
+ |
return match shaped_name(inner)? {
|
|
145 |
+ |
Shaped::Single {
|
|
146 |
+ |
name,
|
|
147 |
+ |
optional: false,
|
|
148 |
+ |
} => Ok(Shaped::Single {
|
|
149 |
+ |
name,
|
|
150 |
+ |
optional: true,
|
|
151 |
+ |
}),
|
|
152 |
+ |
Shaped::Single { .. } => Err(syn::Error::new_spanned(
|
|
153 |
+ |
returns,
|
|
154 |
+ |
"a shape omits its result or does not; there is no second omission",
|
|
155 |
+ |
)),
|
|
156 |
+ |
Shaped::Nodes => Err(syn::Error::new_spanned(
|
|
157 |
+ |
returns,
|
|
158 |
+ |
"a panel with no members is an empty `Vec<Node>`, not a `None`",
|
|
159 |
+ |
)),
|
|
160 |
+ |
};
|
|
161 |
+ |
}
|
|
162 |
+ |
Ok(Shaped::Single {
|
|
163 |
+ |
name: last.ident.to_string(),
|
|
164 |
+ |
optional: false,
|
|
165 |
+ |
})
|
|
166 |
+ |
}
|
|
167 |
+ |
|
|
168 |
+ |
/// The one type inside `Option<..>` or `Vec<..>`.
|
|
169 |
+ |
fn sole_argument<'a>(
|
|
170 |
+ |
returns: &syn::Type,
|
|
171 |
+ |
arguments: &'a syn::PathArguments,
|
|
172 |
+ |
) -> Result<Option<&'a syn::Type>> {
|
|
173 |
+ |
let syn::PathArguments::AngleBracketed(arguments) = arguments else {
|
|
174 |
+ |
return Err(syn::Error::new_spanned(returns, "of what?"));
|
|
175 |
+ |
};
|
|
176 |
+ |
Ok(match arguments.args.first() {
|
|
177 |
+ |
Some(syn::GenericArgument::Type(inner)) => Some(inner),
|
|
178 |
+ |
_ => None,
|
|
179 |
+ |
})
|
| 125 |
180 |
|
}
|
| 126 |
181 |
|
|
| 127 |
182 |
|
/// A shape's body: its bindings, then the single emission that is the value.
|
| 134 |
189 |
|
/// reordered by that: a binding can only name bindings written before it, and
|
| 135 |
190 |
|
/// an emission produces a value rather than an effect, so the two are
|
| 136 |
191 |
|
/// independent within one block.
|
| 137 |
|
- |
fn value_body(items: &[Item], shaped: &str, optional: bool, span: Span) -> Result<TokenStream> {
|
|
192 |
+ |
fn value_body(items: &[Item], shaped: &Shaped, span: Span) -> Result<TokenStream> {
|
|
193 |
+ |
let Shaped::Single {
|
|
194 |
+ |
name: shaped,
|
|
195 |
+ |
optional,
|
|
196 |
+ |
} = shaped
|
|
197 |
+ |
else {
|
|
198 |
+ |
return nodes(items);
|
|
199 |
+ |
};
|
|
200 |
+ |
let optional = *optional;
|
|
201 |
+ |
let shaped = shaped.as_str();
|
| 138 |
202 |
|
let mut bindings = Vec::new();
|
| 139 |
203 |
|
let mut emissions = Vec::new();
|
| 140 |
204 |
|
for item in items {
|
| 467 |
531 |
|
Ok((self::action(action)?, quote!(.activate(#held))))
|
| 468 |
532 |
|
}
|
| 469 |
533 |
|
(Container::Row, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))),
|
|
534 |
+ |
// A control another shape built. `export_act::control` is the export
|
|
535 |
+ |
// portal's one sentence about saving a file, said once and offered
|
|
536 |
+ |
// under each card's own label, and a row holds acts rather than nodes.
|
|
537 |
+ |
(Container::Row, Emission::Include(supplier)) => {
|
|
538 |
+ |
let supplier = hole(supplier)?;
|
|
539 |
+ |
Ok((
|
|
540 |
+ |
quote!(::std::convert::Into::into(#supplier)),
|
|
541 |
+ |
quote!(.act(#held)),
|
|
542 |
+ |
))
|
|
543 |
+ |
}
|
| 470 |
544 |
|
(Container::Row, other) => Err(syn::Error::new(
|
| 471 |
545 |
|
emission_span(other),
|
| 472 |
546 |
|
"a row holds controls and says what opening it does: write \
|
| 473 |
|
- |
`act <label> to <action>;` or `activate to <action>;`",
|
|
547 |
+ |
`act <label> to <action>;`, `include <shape>;` or `activate to <action>;`",
|
| 474 |
548 |
|
)),
|
| 475 |
549 |
|
(Container::Run, Emission::Beside { priority, inner }) => Ok((
|
| 476 |
550 |
|
node(inner)?,
|
| 521 |
595 |
|
"a row of a table holds cells: write `cell <value>;`",
|
| 522 |
596 |
|
)),
|
| 523 |
597 |
|
(Container::Cell, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))),
|
|
598 |
+ |
// A control another shape built, as a row takes one. `item_files`'s
|
|
599 |
+ |
// acts column holds a Download written here and a Delete that is
|
|
600 |
+ |
// `version_delete_act`'s whole subject.
|
|
601 |
+ |
(Container::Cell, Emission::Include(supplier)) => {
|
|
602 |
+ |
let supplier = hole(supplier)?;
|
|
603 |
+ |
Ok((
|
|
604 |
+ |
quote!(::std::convert::Into::into(#supplier)),
|
|
605 |
+ |
quote!(.act(#held)),
|
|
606 |
+ |
))
|
|
607 |
+ |
}
|
| 524 |
608 |
|
(Container::Cell, other) => Err(syn::Error::new(
|
| 525 |
609 |
|
emission_span(other),
|
| 526 |
610 |
|
"a cell holds controls and says what opening it does: write \
|
| 527 |
|
- |
`act <label> to <action>;` or `activate to <action>;`",
|
|
611 |
+ |
`act <label> to <action>;`, `include <shape>;` or `activate to <action>;`",
|
| 528 |
612 |
|
)),
|
| 529 |
613 |
|
(
|
| 530 |
614 |
|
Container::Node,
|
| 816 |
900 |
|
)
|
| 817 |
901 |
|
}
|
| 818 |
902 |
|
|
|
903 |
+ |
/// A panel's members, in order, with nothing wrapping them.
|
|
904 |
+ |
///
|
|
905 |
+ |
/// The `-> Vec<Node>` shape. Kept apart from [`accumulate`] rather than folded
|
|
906 |
+ |
/// into it: every other container is told `.with(member)` and answers with
|
|
907 |
+ |
/// itself, and a `Vec` is told `push` and answers with nothing, so one of the
|
|
908 |
+ |
/// two spellings would have had to be special-cased inside the general form
|
|
909 |
+ |
/// anyway.
|
|
910 |
+ |
fn nodes(items: &[Item]) -> Result<TokenStream> {
|
|
911 |
+ |
let held = format_ident!("nodes", span = Span::call_site());
|
|
912 |
+ |
let statements = node_statements(items, &held)?;
|
|
913 |
+ |
Ok(quote!({
|
|
914 |
+ |
let mut #held = ::std::vec::Vec::new();
|
|
915 |
+ |
#(#statements)*
|
|
916 |
+ |
#held
|
|
917 |
+ |
}))
|
|
918 |
+ |
}
|
|
919 |
+ |
|
|
920 |
+ |
fn node_statements(items: &[Item], nodes: &proc_macro2::Ident) -> Result<Vec<TokenStream>> {
|
|
921 |
+ |
items
|
|
922 |
+ |
.iter()
|
|
923 |
+ |
.map(|item| match item {
|
|
924 |
+ |
Item::Bind { name, source } => {
|
|
925 |
+ |
let value = source_value(source)?;
|
|
926 |
+ |
Ok(quote!(let #name = #value;))
|
|
927 |
+ |
}
|
|
928 |
+ |
Item::For {
|
|
929 |
+ |
dereferenced,
|
|
930 |
+ |
binder,
|
|
931 |
+ |
iterable,
|
|
932 |
+ |
body,
|
|
933 |
+ |
} => {
|
|
934 |
+ |
let iterable = hole(iterable)?;
|
|
935 |
+ |
let inner = node_statements(body, nodes)?;
|
|
936 |
+ |
let binder = binder_pattern(*dereferenced, binder);
|
|
937 |
+ |
Ok(quote!(for #binder in #iterable { #(#inner)* }))
|
|
938 |
+ |
}
|
|
939 |
+ |
Item::Emit(Emission::Guarded { guard, inner }) => {
|
|
940 |
+ |
let test = predicate(guard)?;
|
|
941 |
+ |
let value = node(inner)?;
|
|
942 |
+ |
// R9, as everywhere else: the guard decides whether the member
|
|
943 |
+ |
// is placed, and the holes inside it are evaluated either way.
|
|
944 |
+ |
let member = format_ident!("member", span = Span::call_site());
|
|
945 |
+ |
Ok(quote!({
|
|
946 |
+ |
let #member = #value;
|
|
947 |
+ |
if #test {
|
|
948 |
+ |
#nodes.push(#member);
|
|
949 |
+ |
}
|
|
950 |
+ |
}))
|
|
951 |
+ |
}
|
|
952 |
+ |
Item::Emit(emission) => {
|
|
953 |
+ |
let value = node(emission)?;
|
|
954 |
+ |
Ok(quote!(#nodes.push(#value);))
|
|
955 |
+ |
}
|
|
956 |
+ |
Item::Attribute { name, .. } => Err(syn::Error::new(
|
|
957 |
+ |
name.span(),
|
|
958 |
+ |
"a panel has nothing to set: say it on the member it belongs to",
|
|
959 |
+ |
)),
|
|
960 |
+ |
})
|
|
961 |
+ |
.collect()
|
|
962 |
+ |
}
|
|
963 |
+ |
|
| 819 |
964 |
|
/// The rows of a list, built where they are read.
|
| 820 |
965 |
|
fn list(items: &[Item]) -> Result<TokenStream> {
|
| 821 |
966 |
|
let rows = format_ident!("rows", span = Span::call_site());
|
| 846 |
991 |
|
let binder = binder_pattern(*dereferenced, binder);
|
| 847 |
992 |
|
Ok(quote!(for #binder in #iterable { #(#inner)* }))
|
| 848 |
993 |
|
}
|
| 849 |
|
- |
Item::Emit(Emission::Row { primary, body }) => {
|
| 850 |
|
- |
let row = row(primary, body)?;
|
|
994 |
+ |
// R9 again: the row is built whether or not the guard places it.
|
|
995 |
+ |
Item::Emit(Emission::Guarded { guard, inner }) => {
|
|
996 |
+ |
let test = predicate(guard)?;
|
|
997 |
+ |
let value = list_row(inner)?;
|
|
998 |
+ |
let held = format_ident!("member", span = Span::call_site());
|
|
999 |
+ |
Ok(quote!({
|
|
1000 |
+ |
let #held = #value;
|
|
1001 |
+ |
if #test {
|
|
1002 |
+ |
#rows.push(#held);
|
|
1003 |
+ |
}
|
|
1004 |
+ |
}))
|
|
1005 |
+ |
}
|
|
1006 |
+ |
Item::Emit(emission) => {
|
|
1007 |
+ |
let row = list_row(emission)?;
|
| 851 |
1008 |
|
Ok(quote!(#rows.push(#row);))
|
| 852 |
1009 |
|
}
|
| 853 |
|
- |
Item::Emit(other) => Err(syn::Error::new(
|
| 854 |
|
- |
emission_span(other),
|
| 855 |
|
- |
"a list holds rows: write `row <primary> { .. }`",
|
| 856 |
|
- |
)),
|
| 857 |
1010 |
|
Item::Attribute { name, .. } => Err(syn::Error::new(
|
| 858 |
1011 |
|
name.span(),
|
| 859 |
1012 |
|
"a list has nothing to set: say it on the row",
|
| 862 |
1015 |
|
.collect()
|
| 863 |
1016 |
|
}
|
| 864 |
1017 |
|
|
|
1018 |
+ |
/// One row of a list, written here or built by another shape.
|
|
1019 |
+ |
///
|
|
1020 |
+ |
/// `export_portal` demanded the second: five of its six cards come off a const
|
|
1021 |
+ |
/// and the sixth is the asynchronous export, which is a shape of its own and is
|
|
1022 |
+ |
/// drawn only for a reader who has files. The table already took an `include`
|
|
1023 |
+ |
/// for the same reason, and this is the list saying it the same way.
|
|
1024 |
+ |
fn list_row(emission: &Emission) -> Result<TokenStream> {
|
|
1025 |
+ |
match emission {
|
|
1026 |
+ |
Emission::Row { primary, body } => row(primary, body),
|
|
1027 |
+ |
Emission::Include(supplier) => {
|
|
1028 |
+ |
let supplier = hole(supplier)?;
|
|
1029 |
+ |
Ok(quote!(::std::convert::Into::into(#supplier)))
|
|
1030 |
+ |
}
|
|
1031 |
+ |
other => Err(syn::Error::new(
|
|
1032 |
+ |
emission_span(other),
|
|
1033 |
+ |
"a list holds rows: write `row <primary> { .. }` or `include <shape>;`",
|
|
1034 |
+ |
)),
|
|
1035 |
+ |
}
|
|
1036 |
+ |
}
|
|
1037 |
+ |
|
| 865 |
1038 |
|
/// One table: its columns, its rows, and what it has not shown.
|
| 866 |
1039 |
|
///
|
| 867 |
1040 |
|
/// Both halves accrete. `Table::new` takes its columns as a list and the form
|