Skip to main content

max / quasi

Grow the form for six MNW panels, and the shape that is not one emission `-> Vec<Node>`, demanded by user_support's `body`: the dashboard strip draws the region and its id, so the panel's own fill must add no second one. 52 of the population's 484 shapes return this, third after Slot and Node. The rest are guards and includes, each named by the screen that wanted it: - a region takes a guard (git_notes' panel is `-> Option<Node>` and the whole region is what the emptiness drops) - a column and a cell take a guard (git_repos shows a visibility column to the owner and to nobody else, and the same predicate decides the cell) - a guard on a `beside` is lifted outside it, because the run is what may hold nothing (git_notes' header, two of three members conditional) - a list takes an include and a guard (export_portal's asynchronous card) - a row and a cell take an include, which is a control another shape built (export_act::control, version_delete_act::act) - `literal` as a node member (git_notes' namespace is a ref path, not prose) quasi-router 0.101.7 closes two gaps of the shape every earlier one had, a variant with no constructor: `Node::literal` for an inline unclassified run, and `RegionKind::handover` beside `Slot::handover`, which a declaration needs because it always holds the id separately.
Author: Max Johnson <me@maxj.phd> · 2026-09-03 20:14 UTC
Signed with PGP, not checked
Commit: eafe6e439bdba476972fed3c1206c9b070f3eea7
Parent: 3fe4f1d
4 files changed, +279 insertions, -47 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-router"
3 - version = "0.101.6"
3 + version = "0.101.7"
4 4 description = "Host-agnostic router: a request in, a renderer-agnostic description out"
5 5 edition.workspace = true
6 6 rust-version.workspace = true
@@ -59,7 +59,7 @@
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,17 +71,22 @@
71 71 let ty = &param.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,11 +96,25 @@
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,23 +124,59 @@
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,7 +189,16 @@
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,10 +531,20 @@
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,10 +595,20 @@
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,6 +900,67 @@
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,14 +991,22 @@
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,6 +1015,26 @@
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
@@ -62,6 +62,7 @@
62 62 "failed",
63 63 "empty",
64 64 "rich",
65 + "literal",
65 66 ];
66 67
67 68 /// The arrangements a screen may be laid out in.
@@ -415,19 +416,38 @@
415 416 match member.to_string().as_str() {
416 417 "beside" => {
417 418 let priority: Ident = input.parse()?;
418 - Ok(Self::Beside {
419 - priority,
420 - inner: Box::new(input.parse()?),
419 + let inner: Self = input.parse()?;
420 + // A guard on a placed member guards the placing. The run is
421 + // what may hold nothing and `beside` is how a member reaches
422 + // it, so the guard belongs outside: read the other way round
423 + // it asks a run to hold an absence, which nothing can do.
424 + Ok(match inner {
425 + Self::Guarded { guard, inner } => Self::Guarded {
426 + guard,
427 + inner: Box::new(Self::Beside { priority, inner }),
428 + },
429 + inner => Self::Beside {
430 + priority,
431 + inner: Box::new(inner),
432 + },
421 433 })
422 434 }
423 435 "region" => {
424 436 let name: Arg = input.parse()?;
425 437 input.parse::<Token![as]>()?;
426 - Ok(Self::Region {
427 - name,
428 - kind: input.parse()?,
429 - body: block(input)?,
430 - })
438 + let kind: RegionKind = input.parse()?;
439 + // The guard sits between the kind and the body, where `act`
440 + // puts its own: a region that is sometimes not there is the
441 + // same fact as a control that is sometimes not offered.
442 + let guard = guard(input)?;
443 + Ok(guarded(
444 + guard,
445 + Self::Region {
446 + name,
447 + kind,
448 + body: block(input)?,
449 + },
450 + ))
431 451 }
432 452 "across" => {
433 453 let fallback: Ident = input.parse()?;
@@ -514,13 +534,18 @@
514 534 "table" => Ok(Self::Table(block(input)?)),
515 535 "column" => {
516 536 let name: Arg = input.parse()?;
537 + // Before the body, where every other guarded member puts it.
538 + // `git_repos` shows a visibility column to the owner and to
539 + // nobody else, and the same guard decides the cell below, so
540 + // the two cannot fall out of step.
541 + let guard = guard(input)?;
517 542 let body = if input.peek(token::Brace) {
518 543 block(input)?
519 544 } else {
520 545 input.parse::<Token![;]>()?;
521 546 Vec::new()
522 547 };
523 - Ok(Self::Column { name, body })
548 + Ok(guarded(guard, Self::Column { name, body }))
524 549 }
525 550 "cells" => Ok(Self::Cells(block(input)?)),
526 551 "cell" => {
@@ -531,17 +556,21 @@
531 556 None
532 557 };
533 558 let value: Arg = input.parse()?;
559 + let guard = guard(input)?;
534 560 let body = if input.peek(token::Brace) {
535 561 block(input)?
536 562 } else {
537 563 input.parse::<Token![;]>()?;
538 564 Vec::new()
539 565 };
540 - Ok(Self::Cell {
541 - column,
542 - value,
543 - body,
544 - })
566 + Ok(guarded(
567 + guard,
568 + Self::Cell {
569 + column,
570 + value,
571 + body,
572 + },
573 + ))
545 574 }
546 575 "offering" => {
547 576 let label: Arg = input.parse()?;
@@ -4182,6 +4182,17 @@
4182 4182 }
4183 4183
4184 4184 impl RegionKind {
4185 + /// A place the app fills itself, and owes every host a fill for.
4186 + ///
4187 + /// Beside [`Slot::handover`], which builds the region and its id in one
4188 + /// call. This is the kind on its own, which is what a caller holding the id
4189 + /// separately needs: the three kinds that carry a name are the three a bare
4190 + /// variant cannot spell.
4191 + #[must_use]
4192 + pub fn handover(name: impl Into<String>) -> Self {
4193 + Self::Handover { name: name.into() }
4194 + }
4195 +
4185 4196 /// Borrow as the description layer's own type.
4186 4197 #[must_use]
4187 4198 pub fn as_layout(&self) -> layout::Region<'_> {
@@ -8633,6 +8644,25 @@
8633 8644 }
8634 8645 }
8635 8646
8647 + /// Machine text in a line of reading.
8648 + ///
8649 + /// One unclassified run, inline, no language: a ref path, a fingerprint, a
8650 + /// clone URL, a line of a file. [`Code`](Self::Code) is the only node with
8651 + /// no constructor of its own, and six sites in the tree write this exact
8652 + /// literal out -- one of them under a private helper called `literal`,
8653 + /// which is where the name comes from.
8654 + ///
8655 + /// Nothing lexed it and nothing should, so what this buys is the monospace
8656 + /// and not a colour. A block, or runs a lexer classified, still writes the
8657 + /// variant; the screen that asks for either earns the constructor for it.
8658 + pub fn literal(text: impl Into<String>) -> Self {
8659 + Self::Code {
8660 + runs: ::std::vec![Lexeme::plain(text)],
8661 + language: None,
8662 + inline: true,
8663 + }
8664 + }
8665 +
8636 8666 /// Prose written in markdown, by somebody the app does not vouch for.
8637 8667 ///
8638 8668 /// [`Richness::Sentence`] and [`Trust::Untrusted`], which is what this member