max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
5 files changed,
+155 insertions,
-30 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "quasi-router" | |
| 3 | - | version = "0.101.2" | |
| 3 | + | version = "0.101.3" | |
| 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 |
| @@ -201,6 +201,15 @@ | |||
| 201 | 201 | } | |
| 202 | 202 | ||
| 203 | 203 | pub enum Predicate { | |
| 204 | + | /// `<clause> and <clause>`, or `<clause> or <clause>`. One connective per | |
| 205 | + | /// predicate: mixing them would need precedence, and precedence is how a | |
| 206 | + | /// predicate becomes an expression. | |
| 207 | + | Joined { | |
| 208 | + | connective: Ident, | |
| 209 | + | clauses: Vec<Predicate>, | |
| 210 | + | }, | |
| 211 | + | /// `not <clause>`. | |
| 212 | + | Not(Box<Predicate>), | |
| 204 | 213 | /// A hole that is already a `bool`. | |
| 205 | 214 | Truth(Hole), | |
| 206 | 215 | /// `<hole> <compare> <arg>`. The comparison is a word rather than an |
| @@ -239,7 +239,15 @@ | |||
| 239 | 239 | ||
| 240 | 240 | /// A body that accretes onto something already built. | |
| 241 | 241 | fn accrete(items: &[Item], container: Container, base: &TokenStream) -> Result<TokenStream> { | |
| 242 | - | if items.iter().any(|item| matches!(item, Item::For { .. })) { | |
| 242 | + | // A loop cannot be a link in a chain, and neither can a guard: both are | |
| 243 | + | // statements. A body with either accumulates instead. | |
| 244 | + | let statementish = items.iter().any(|item| { | |
| 245 | + | matches!( | |
| 246 | + | item, | |
| 247 | + | Item::For { .. } | Item::Emit(Emission::Guarded { .. }) | |
| 248 | + | ) | |
| 249 | + | }); | |
| 250 | + | if statementish { | |
| 243 | 251 | return accumulate(items, container, base); | |
| 244 | 252 | } | |
| 245 | 253 | ||
| @@ -311,6 +319,21 @@ | |||
| 311 | 319 | let inner = self::statements(body, container, built)?; | |
| 312 | 320 | quote!(for #binder in #iterable { #(#inner)* }) | |
| 313 | 321 | } | |
| 322 | + | Item::Emit(Emission::Guarded { guard, inner }) => { | |
| 323 | + | let test = predicate(guard)?; | |
| 324 | + | let held = format_ident!("member_{index}", span = Span::call_site()); | |
| 325 | + | let (value, call) = step(inner, container, &held)?; | |
| 326 | + | // R9: the guard decides whether the member is placed, not | |
| 327 | + | // whether its holes are evaluated. The value is built either | |
| 328 | + | // way, and a supplier that is asked for nothing answers with | |
| 329 | + | // nothing. | |
| 330 | + | quote!({ | |
| 331 | + | let #held = #value; | |
| 332 | + | if #test { | |
| 333 | + | #built = #built #call; | |
| 334 | + | } | |
| 335 | + | }) | |
| 336 | + | } | |
| 314 | 337 | Item::Emit(emission) => { | |
| 315 | 338 | let held = format_ident!("member_{index}", span = Span::call_site()); | |
| 316 | 339 | let (value, call) = step(emission, container, &held)?; | |
| @@ -405,7 +428,37 @@ | |||
| 405 | 428 | ||
| 406 | 429 | /// One guard, as the `bool` it tests. | |
| 407 | 430 | fn predicate(guard: &Guard) -> Result<TokenStream> { | |
| 408 | - | let test = match &guard.predicate { | |
| 431 | + | let test = clause(&guard.predicate)?; | |
| 432 | + | Ok(if guard.negated { | |
| 433 | + | quote!(!(#test)) | |
| 434 | + | } else { | |
| 435 | + | test | |
| 436 | + | }) | |
| 437 | + | } | |
| 438 | + | ||
| 439 | + | fn clause(predicate: &Predicate) -> Result<TokenStream> { | |
| 440 | + | Ok(match predicate { | |
| 441 | + | Predicate::Joined { | |
| 442 | + | connective, | |
| 443 | + | clauses, | |
| 444 | + | } => { | |
| 445 | + | let operator: TokenStream = if connective == "and" { "&&" } else { "||" } | |
| 446 | + | .parse() | |
| 447 | + | .expect("a connective"); | |
| 448 | + | let clauses = clauses.iter().map(clause).collect::<Result<Vec<_>>>()?; | |
| 449 | + | let mut joined = TokenStream::new(); | |
| 450 | + | for (index, one) in clauses.into_iter().enumerate() { | |
| 451 | + | if index > 0 { | |
| 452 | + | joined.extend(operator.clone()); | |
| 453 | + | } | |
| 454 | + | joined.extend(quote!((#one))); | |
| 455 | + | } | |
| 456 | + | joined | |
| 457 | + | } | |
| 458 | + | Predicate::Not(inner) => { | |
| 459 | + | let inner = clause(inner)?; | |
| 460 | + | quote!(!(#inner)) | |
| 461 | + | } | |
| 409 | 462 | Predicate::Truth(hole) => self::hole(hole)?, | |
| 410 | 463 | Predicate::Comparison { | |
| 411 | 464 | left, | |
| @@ -425,11 +478,6 @@ | |||
| 425 | 478 | let right = arg(right)?; | |
| 426 | 479 | quote!(#left #operator #right) | |
| 427 | 480 | } | |
| 428 | - | }; | |
| 429 | - | Ok(if guard.negated { | |
| 430 | - | quote!(!(#test)) | |
| 431 | - | } else { | |
| 432 | - | test | |
| 433 | 481 | }) | |
| 434 | 482 | } | |
| 435 | 483 |
| @@ -47,7 +47,16 @@ | |||
| 47 | 47 | ||
| 48 | 48 | /// The members that are one of `Node`'s own constructors, called by its name. | |
| 49 | 49 | /// Growing this list is the whole of adding one. | |
| 50 | - | const NODE_MEMBERS: &[&str] = &["text", "page", "section", "list"]; | |
| 50 | + | const NODE_MEMBERS: &[&str] = &[ | |
| 51 | + | "text", | |
| 52 | + | "page", | |
| 53 | + | "section", | |
| 54 | + | "subsection", | |
| 55 | + | "list", | |
| 56 | + | "stats", | |
| 57 | + | "banner", | |
| 58 | + | "failed", | |
| 59 | + | ]; | |
| 51 | 60 | ||
| 52 | 61 | /// The arrangements a screen may be laid out in. | |
| 53 | 62 | const ARRANGEMENTS: &[&str] = &["single", "list_detail", "sidebar_content"]; | |
| @@ -520,33 +529,79 @@ | |||
| 520 | 529 | return Ok(None); | |
| 521 | 530 | }; | |
| 522 | 531 | let word: Ident = input.parse()?; | |
| 523 | - | ||
| 524 | - | let left: Hole = input.parse()?; | |
| 525 | - | let predicate = if input.peek(Ident) { | |
| 526 | - | let compare: Ident = input.parse()?; | |
| 527 | - | let spelling = compare.to_string(); | |
| 528 | - | if !COMPARISONS.iter().any(|(known, _)| *known == spelling) { | |
| 529 | - | return Err(syn::Error::new( | |
| 530 | - | compare.span(), | |
| 531 | - | format!("`{spelling}` is not one of the comparisons"), | |
| 532 | - | )); | |
| 533 | - | } | |
| 534 | - | Predicate::Comparison { | |
| 535 | - | left, | |
| 536 | - | compare, | |
| 537 | - | right: input.parse()?, | |
| 538 | - | } | |
| 539 | - | } else { | |
| 540 | - | Predicate::Truth(left) | |
| 541 | - | }; | |
| 542 | - | ||
| 543 | 532 | Ok(Some(Guard { | |
| 544 | 533 | negated, | |
| 545 | - | predicate, | |
| 534 | + | predicate: predicate(input)?, | |
| 546 | 535 | span: word.span(), | |
| 547 | 536 | })) | |
| 548 | 537 | } | |
| 549 | 538 | ||
| 539 | + | /// One predicate: clauses joined by one connective, and never two. | |
| 540 | + | fn predicate(input: ParseStream) -> Result<Predicate> { | |
| 541 | + | let first = clause(input)?; | |
| 542 | + | let Some(connective) = connective_ahead(input)? else { | |
| 543 | + | return Ok(first); | |
| 544 | + | }; | |
| 545 | + | let mut clauses = vec![first]; | |
| 546 | + | loop { | |
| 547 | + | let Some(next) = connective_ahead(input)? else { | |
| 548 | + | break; | |
| 549 | + | }; | |
| 550 | + | if next != connective { | |
| 551 | + | return Err(syn::Error::new( | |
| 552 | + | input.span(), | |
| 553 | + | "one connective per predicate: parenthesise to mix `and` with `or`", | |
| 554 | + | )); | |
| 555 | + | } | |
| 556 | + | input.parse::<Ident>()?; | |
| 557 | + | clauses.push(clause(input)?); | |
| 558 | + | } | |
| 559 | + | Ok(Predicate::Joined { | |
| 560 | + | connective, | |
| 561 | + | clauses, | |
| 562 | + | }) | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | /// Whether `and` or `or` continues the predicate here. | |
| 566 | + | fn connective_ahead(input: ParseStream) -> Result<Option<Ident>> { | |
| 567 | + | if !input.peek(Ident) { | |
| 568 | + | return Ok(None); | |
| 569 | + | } | |
| 570 | + | let word = input.fork().parse::<Ident>()?; | |
| 571 | + | Ok(match word.to_string().as_str() { | |
| 572 | + | "and" | "or" => Some(word), | |
| 573 | + | _ => None, | |
| 574 | + | }) | |
| 575 | + | } | |
| 576 | + | ||
| 577 | + | fn clause(input: ParseStream) -> Result<Predicate> { | |
| 578 | + | if input.peek(Ident) && input.fork().parse::<Ident>()? == "not" { | |
| 579 | + | input.parse::<Ident>()?; | |
| 580 | + | return Ok(Predicate::Not(Box::new(clause(input)?))); | |
| 581 | + | } | |
| 582 | + | if input.peek(token::Paren) { | |
| 583 | + | let inner; | |
| 584 | + | parenthesized!(inner in input); | |
| 585 | + | return predicate(&inner); | |
| 586 | + | } | |
| 587 | + | ||
| 588 | + | let left: Hole = input.parse()?; | |
| 589 | + | if !input.peek(Ident) { | |
| 590 | + | return Ok(Predicate::Truth(left)); | |
| 591 | + | } | |
| 592 | + | let word = input.fork().parse::<Ident>()?; | |
| 593 | + | let spelling = word.to_string(); | |
| 594 | + | if !COMPARISONS.iter().any(|(known, _)| *known == spelling) { | |
| 595 | + | return Ok(Predicate::Truth(left)); | |
| 596 | + | } | |
| 597 | + | let compare: Ident = input.parse()?; | |
| 598 | + | Ok(Predicate::Comparison { | |
| 599 | + | left, | |
| 600 | + | compare, | |
| 601 | + | right: input.parse()?, | |
| 602 | + | }) | |
| 603 | + | } | |
| 604 | + | ||
| 550 | 605 | fn guarded(guard: Option<Guard>, inner: Emission) -> Emission { | |
| 551 | 606 | match guard { | |
| 552 | 607 | Some(guard) => Emission::Guarded { |
| @@ -8554,6 +8554,19 @@ | |||
| 8554 | 8554 | } | |
| 8555 | 8555 | } | |
| 8556 | 8556 | ||
| 8557 | + | /// A subsection title. | |
| 8558 | + | /// | |
| 8559 | + | /// The third of the three heading levels, which had no constructor while | |
| 8560 | + | /// the other two did. Every site that wanted one wrote the variant out | |
| 8561 | + | /// with its `level` and its `text`, which is the struct rather than the | |
| 8562 | + | /// vocabulary. | |
| 8563 | + | pub fn subsection(text: impl Into<String>) -> Self { | |
| 8564 | + | Self::Heading { | |
| 8565 | + | level: layout::Heading::Subsection, | |
| 8566 | + | text: text.into(), | |
| 8567 | + | } | |
| 8568 | + | } | |
| 8569 | + | ||
| 8557 | 8570 | /// Ordinary prose. | |
| 8558 | 8571 | pub fn text(text: impl Into<String>) -> Self { | |
| 8559 | 8572 | Self::Text { |