Skip to main content

max / quasi

Let a screen take its arrangement's arguments, and a question stand alone Four productions, all from MNW's pricing page: the first converted screen that owns its whole document and the only one whose region consults. - A screen's arrangement takes the arguments it needs. Screen::list_detail takes a title and whether it is tabbed, and the grammar took exactly one because every screen converted before this was single. - A field may stand in a region. Refused until now with "put it in a form, or give the shape -> Field", and that was right while every question was submitted. A consulting region gathers the dials it contains and sends them itself, so there is no form: pricing's calculator is five dials and no submit button anywhere on the page. Narrow on purpose -- a field is still refused in a cell or a row, where nothing would read it. - A row takes a guard, and a field takes a guard, both before the body where column, cell, act, offering and form already put theirs. pricing's footer links Changelog only while one is published, and asks the founder-or-list question only while there are two rates. quasi-router 0.101.10 adds Node::toned(text, tone). Node::text is that at Tone::Neutral and was the only constructor Text had, so a line that is a warning wrote the struct variant out by hand at 21 production sites, nine of them in MNW's commit view. Not a notice: a notice is a thing that happened and carries a way out, where this is a sentence in the reading that is warning-coloured. Consumers pin version = "0.101", so nothing is owed a forward fix.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-04 15:53 UTC
Signed with PGP, not checked
Commit: 2353c9e7f3a2f3ea47a4b51d43df0d5809d168ec
Parent: 9a8a8c3
5 files changed, +83 insertions, -14 deletions
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "quasi-router"
3 - version = "0.101.9"
3 + version = "0.101.10"
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
@@ -164,7 +164,10 @@
164 164 /// `screen <arrangement> <arg> { .. }`
165 165 Screen {
166 166 arrangement: Ident,
167 - title: Arg,
167 + /// What the arrangement is told. Every one takes a title; `list_detail`
168 + /// takes whether it is tabbed as well, which is why this is a list
169 + /// rather than the one title it was until wave 9.
170 + args: Vec<Arg>,
168 171 body: Vec<Item>,
169 172 },
170 173 /// `row <arg> { .. }`
@@ -262,14 +262,14 @@
262 262 "Screen" => match only {
263 263 Emission::Screen {
264 264 arrangement,
265 - title,
265 + args,
266 266 body,
267 267 } => {
268 - let title = arg(title)?;
268 + let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?;
269 269 accrete(
270 270 body,
271 271 Container::Screen,
272 - &quote!(::quasi_router::Screen::#arrangement(#title)),
272 + &quote!(::quasi_router::Screen::#arrangement(#(#args),*)),
273 273 )?
274 274 }
275 275 other => {
@@ -523,6 +523,30 @@
523 523 )?;
524 524 Ok((run, quote!(.across(#held))))
525 525 }
526 + // A question standing in a region rather than in a form. A region that
527 + // consults gathers the dials it contains and sends them itself, so
528 + // there is no form to put them in and `Node::field` is what the
529 + // vocabulary has for it. `pricing`'s calculator is the site: five dials
530 + // and no submit button anywhere on the page.
531 + //
532 + // Narrow on purpose. Everywhere else a field is still refused with the
533 + // message that names the two legal homes, because a question in a cell
534 + // or a row is a question nothing will ever read.
535 + (
536 + Container::Slot,
537 + Emission::Field {
538 + kind,
539 + name,
540 + label,
541 + body,
542 + },
543 + ) => {
544 + let built = field(kind, name, label, body)?;
545 + Ok((
546 + quote!(::quasi_router::Node::field(#built)),
547 + quote!(.with(#held)),
548 + ))
549 + }
526 550 (Container::Slot, other) => Ok((node(other)?, quote!(.with(#held)))),
527 551 (Container::Screen, Emission::Region { name, kind, body }) => {
528 552 Ok((slot(name, kind, body)?, quote!(.with(#held))))
@@ -76,6 +76,7 @@
76 76 /// it.
77 77 const NODE_MEMBERS: &[&str] = &[
78 78 "text",
79 + "toned",
79 80 "page",
80 81 "section",
81 82 "subsection",
@@ -549,22 +550,35 @@
549 550 "a screen is laid out `single`, `list_detail` or `sidebar_content`",
550 551 ));
551 552 }
552 - let title: Arg = input.parse()?;
553 + // Every arrangement takes a title, and `list_detail` takes
554 + // whether it is tabbed as well. Read to the body rather than
555 + // taking exactly one, so the grammar says what the constructor
556 + // says. `pricing` is the site: the first converted screen that
557 + // is not `single`.
558 + let mut args = Vec::new();
559 + while !input.peek(token::Brace) {
560 + args.push(input.parse()?);
561 + }
553 562 Ok(Self::Screen {
554 563 arrangement,
555 - title,
564 + args,
556 565 body: block(input)?,
557 566 })
558 567 }
559 568 "row" => {
560 569 let primary: Arg = input.parse()?;
570 + // Before the body, where `column`, `cell`, `act`, `offering`
571 + // and `form` already put theirs. `pricing`'s footer is the
572 + // site: Changelog is linked only while a published changelog
573 + // project exists, and the route 404s otherwise.
574 + let guard = guard(input)?;
561 575 let body = if input.peek(token::Brace) {
562 576 block(input)?
563 577 } else {
564 578 input.parse::<Token![;]>()?;
565 579 Vec::new()
566 580 };
567 - Ok(Self::Row { primary, body })
581 + Ok(guarded(guard, Self::Row { primary, body }))
568 582 }
569 583 // The guard goes after the body here rather than before it, because
570 584 // the body is what tells this `list` from the node member of the
@@ -667,18 +681,27 @@
667 681 let kind: Ident = input.parse()?;
668 682 let name: Arg = input.parse()?;
669 683 let label: Arg = input.parse()?;
684 + // Before the body, where every other guarded member puts it.
685 + // `pricing` is the site: the founder-or-list question is asked
686 + // only while there are two rates to choose between, and with
687 + // the window shut the page is what it was before the question
688 + // existed.
689 + let guard = guard(input)?;
670 690 let body = if input.peek(token::Brace) {
671 691 block(input)?
672 692 } else {
673 693 input.parse::<Token![;]>()?;
674 694 Vec::new()
675 695 };
676 - Ok(Self::Field {
677 - kind,
678 - name,
679 - label,
680 - body,
681 - })
696 + Ok(guarded(
697 + guard,
698 + Self::Field {
699 + kind,
700 + name,
701 + label,
702 + body,
703 + },
704 + ))
682 705 }
683 706 name if NODE_MEMBERS.contains(&name) => {
684 707 let mut args = Vec::new();
@@ -8680,6 +8680,25 @@
8680 8680 }
8681 8681 }
8682 8682
8683 + /// The same prose, saying something about itself.
8684 + ///
8685 + /// [`text`](Self::text) is this at [`layout::Tone::Neutral`], which is
8686 + /// ordinary content, and it was the only constructor `Text` had: a line
8687 + /// that is a warning wrote the variant out by hand. **21 sites in the tree
8688 + /// do**, nine of them in MNW's commit view alone, which makes this the most
8689 + /// repeated instance of the gap `Node::literal` and `Node::token` closed
8690 + /// before it.
8691 + ///
8692 + /// Not a notice. A notice is a thing that happened and carries a way out;
8693 + /// this is a sentence in the reading that is warning-coloured, which is
8694 + /// what a diff's deletion count and an unverifiable signature both are.
8695 + pub fn toned(text: impl Into<String>, tone: layout::Tone) -> Self {
8696 + Self::Text {
8697 + text: text.into(),
8698 + tone,
8699 + }
8700 + }
8701 +
8683 8702 /// Machine text in a line of reading.
8684 8703 ///
8685 8704 /// One unclassified run, inline, no language: a ref path, a fingerprint, a