max / quasi
- Record
- wiki quasi-declare-form section 13.
5 files changed,
+231 insertions,
-18 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "quasi-router" | |
| 3 | - | version = "0.101.4" | |
| 3 | + | version = "0.101.5" | |
| 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 |
| @@ -36,13 +36,23 @@ | |||
| 36 | 36 | pub enum Item { | |
| 37 | 37 | /// `let name = <source>;` | |
| 38 | 38 | Bind { name: Ident, source: Source }, | |
| 39 | - | /// `<builder> <arg>*;` -- one setting on the enclosing container. | |
| 39 | + | /// `<builder> <arg>* <guard>?;` -- one setting on the enclosing container. | |
| 40 | 40 | /// | |
| 41 | 41 | /// The ident is the **builder method's** name and not the struct field's, | |
| 42 | 42 | /// which is what ATTRIBUTE NAMING settles: `Slot::named` writes `name`, and | |
| 43 | 43 | /// a form that spelled the field would be naming something no caller can | |
| 44 | 44 | /// reach. | |
| 45 | - | Attribute { name: Ident, args: Vec<Arg> }, | |
| 45 | + | /// | |
| 46 | + | /// A setting takes a guard for the same reason a member does: the rule is | |
| 47 | + | /// that a guard is allowed where something may be absent, and a setting | |
| 48 | + | /// that is sometimes not made is exactly that. `git_explore`'s `more` is | |
| 49 | + | /// the live instance -- a table says what it has not shown only when there | |
| 50 | + | /// is another page. | |
| 51 | + | Attribute { | |
| 52 | + | name: Ident, | |
| 53 | + | args: Vec<Arg>, | |
| 54 | + | guard: Option<Guard>, | |
| 55 | + | }, | |
| 46 | 56 | /// `for <binder> in <hole> { .. }` -- the same body once per element. | |
| 47 | 57 | /// | |
| 48 | 58 | /// A loop is the one item that cannot be hoisted above its container: its | |
| @@ -171,6 +181,29 @@ | |||
| 171 | 181 | action: Action, | |
| 172 | 182 | body: Vec<Item>, | |
| 173 | 183 | }, | |
| 184 | + | /// `table { column ..; cells { .. } }` | |
| 185 | + | /// | |
| 186 | + | /// Columns and rows accrete rather than arriving as two lists, which is | |
| 187 | + | /// what `Table::column` and `Cells::cell` were added to `quasi-router` for. | |
| 188 | + | /// The form has no expression to hold a list in, and a production that | |
| 189 | + | /// admitted one would be the door this grammar exists to keep shut. | |
| 190 | + | Table(Vec<Item>), | |
| 191 | + | /// `column <name> ( "{" .. "}" | ";" )` -- one column and how it narrows. | |
| 192 | + | Column { name: Arg, body: Vec<Item> }, | |
| 193 | + | /// `cells { cell ..; }` -- one row of a table, by position. | |
| 194 | + | /// | |
| 195 | + | /// Positional only. A cell that names its column (`Cells::at`) is not a | |
| 196 | + | /// production yet: `git_blame`'s row is the one site that wants it, and it | |
| 197 | + | /// is still hand-written. | |
| 198 | + | Cells(Vec<Item>), | |
| 199 | + | /// `cell <value> ( "{" .. "}" | ";" )` | |
| 200 | + | Cell { value: Arg, body: Vec<Item> }, | |
| 201 | + | /// `activate to <action>;` -- what opening this row or cell does. | |
| 202 | + | /// | |
| 203 | + | /// A setting in shape, and a member in the tree, because the thing it sets | |
| 204 | + | /// is an action and a setting's arguments are args. Rows, rows of cells and | |
| 205 | + | /// single cells all carry one. | |
| 206 | + | Activate(Action), | |
| 174 | 207 | /// `include <hole>;` -- whatever another shape built. | |
| 175 | 208 | Include(Hole), | |
| 176 | 209 | /// `link <arg> to <action>;` |
| @@ -36,6 +36,14 @@ | |||
| 36 | 36 | Screen, | |
| 37 | 37 | /// One row of a list: its members are the controls it offers. | |
| 38 | 38 | Row, | |
| 39 | + | /// A table: its members are its columns and its rows. | |
| 40 | + | Table, | |
| 41 | + | /// One column: its body says how it narrows, and it emits nothing. | |
| 42 | + | Column, | |
| 43 | + | /// One row of a table: its members are its cells, by position. | |
| 44 | + | Cells, | |
| 45 | + | /// One cell: its body says what opening it does. | |
| 46 | + | Cell, | |
| 39 | 47 | } | |
| 40 | 48 | ||
| 41 | 49 | pub fn declaration(declaration: &Declaration) -> Result<TokenStream> { | |
| @@ -255,7 +263,9 @@ | |||
| 255 | 263 | let statementish = items.iter().any(|item| { | |
| 256 | 264 | matches!( | |
| 257 | 265 | item, | |
| 258 | - | Item::For { .. } | Item::Emit(Emission::Guarded { .. }) | |
| 266 | + | Item::For { .. } | |
| 267 | + | | Item::Emit(Emission::Guarded { .. }) | |
| 268 | + | | Item::Attribute { guard: Some(_), .. } | |
| 259 | 269 | ) | |
| 260 | 270 | }); | |
| 261 | 271 | if statementish { | |
| @@ -270,7 +280,7 @@ | |||
| 270 | 280 | let value = source_value(source)?; | |
| 271 | 281 | bindings.push(quote!(let #name = #value;)); | |
| 272 | 282 | } | |
| 273 | - | Item::Attribute { name, args } => steps.push(attribute(name, args)?), | |
| 283 | + | Item::Attribute { name, args, .. } => steps.push(attribute(name, args)?), | |
| 274 | 284 | Item::For { .. } => unreachable!("a loop takes the accumulating form"), | |
| 275 | 285 | Item::Emit(emission) => { | |
| 276 | 286 | // A member is evaluated into its own binding before the | |
| @@ -317,9 +327,18 @@ | |||
| 317 | 327 | let value = source_value(source)?; | |
| 318 | 328 | quote!(let #name = #value;) | |
| 319 | 329 | } | |
| 320 | - | Item::Attribute { name, args } => { | |
| 330 | + | Item::Attribute { name, args, guard } => { | |
| 321 | 331 | let call = attribute(name, args)?; | |
| 322 | - | quote!(#built = #built #call;) | |
| 332 | + | match guard { | |
| 333 | + | // The same rule a guarded member follows: the guard decides | |
| 334 | + | // whether the setting is made, and the args are evaluated | |
| 335 | + | // either way. | |
| 336 | + | Some(guard) => { | |
| 337 | + | let test = predicate(guard)?; | |
| 338 | + | quote!(if #test { #built = #built #call; }) | |
| 339 | + | } | |
| 340 | + | None => quote!(#built = #built #call;), | |
| 341 | + | } | |
| 323 | 342 | } | |
| 324 | 343 | Item::For { | |
| 325 | 344 | dereferenced, | |
| @@ -419,10 +438,14 @@ | |||
| 419 | 438 | emission_span(other), | |
| 420 | 439 | "a document holds regions: write `region <name> as <kind> { .. }`", | |
| 421 | 440 | )), | |
| 441 | + | (Container::Cells | Container::Cell | Container::Row, Emission::Activate(action)) => { | |
| 442 | + | Ok((self::action(action)?, quote!(.activate(#held)))) | |
| 443 | + | } | |
| 422 | 444 | (Container::Row, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))), | |
| 423 | 445 | (Container::Row, other) => Err(syn::Error::new( | |
| 424 | 446 | emission_span(other), | |
| 425 | - | "a row holds controls: write `act <label> to <action>;`", | |
| 447 | + | "a row holds controls and says what opening it does: write \ | |
| 448 | + | `act <label> to <action>;` or `activate to <action>;`", | |
| 426 | 449 | )), | |
| 427 | 450 | (Container::Run, Emission::Beside { priority, inner }) => Ok(( | |
| 428 | 451 | node(inner)?, | |
| @@ -432,6 +455,29 @@ | |||
| 432 | 455 | emission_span(other), | |
| 433 | 456 | "a row ranks what it holds: write `beside <priority> <emission>`", | |
| 434 | 457 | )), | |
| 458 | + | (Container::Table, Emission::Column { name, body }) => { | |
| 459 | + | Ok((column(name, body)?, quote!(.column(#held)))) | |
| 460 | + | } | |
| 461 | + | (Container::Table, Emission::Cells(body)) => Ok((cells(body)?, quote!(.row(#held)))), | |
| 462 | + | (Container::Table, other) => Err(syn::Error::new( | |
| 463 | + | emission_span(other), | |
| 464 | + | "a table holds columns and rows: write `column <name>;` or `cells { .. }`", | |
| 465 | + | )), | |
| 466 | + | (Container::Cells, Emission::Cell { value, body }) => { | |
| 467 | + | Ok((cell(value, body)?, quote!(.cell(#held)))) | |
| 468 | + | } | |
| 469 | + | (Container::Cells, other) => Err(syn::Error::new( | |
| 470 | + | emission_span(other), | |
| 471 | + | "a row of a table holds cells: write `cell <value>;`", | |
| 472 | + | )), | |
| 473 | + | (Container::Cell, other) => Err(syn::Error::new( | |
| 474 | + | emission_span(other), | |
| 475 | + | "a cell says what opening it does, and nothing else yet", | |
| 476 | + | )), | |
| 477 | + | (Container::Column, other) => Err(syn::Error::new( | |
| 478 | + | emission_span(other), | |
| 479 | + | "a column holds no members: its body says how it narrows", | |
| 480 | + | )), | |
| 435 | 481 | (Container::Act | Container::Field, other) => Err(syn::Error::new( | |
| 436 | 482 | emission_span(other), | |
| 437 | 483 | "this holds no members: its body says what it is like", | |
| @@ -502,6 +548,10 @@ | |||
| 502 | 548 | Ok(quote!(::quasi_router::Node::#member(#(#args),*))) | |
| 503 | 549 | } | |
| 504 | 550 | Emission::List(items) => list(items), | |
| 551 | + | Emission::Table(items) => { | |
| 552 | + | let table = self::table(items)?; | |
| 553 | + | Ok(quote!(::std::convert::Into::into(#table))) | |
| 554 | + | } | |
| 505 | 555 | Emission::Form { action, body } => form(action, body), | |
| 506 | 556 | Emission::Field { kind, .. } => Err(syn::Error::new( | |
| 507 | 557 | kind.span(), | |
| @@ -569,6 +619,16 @@ | |||
| 569 | 619 | fallback.span(), | |
| 570 | 620 | "a row is not a node: `across` belongs in a region", | |
| 571 | 621 | )), | |
| 622 | + | Emission::Column { .. } | Emission::Cells(_) | Emission::Cell { .. } => { | |
| 623 | + | Err(syn::Error::new( | |
| 624 | + | emission_span(emission), | |
| 625 | + | "a column, a row of cells and a cell are a table's, not a node's", | |
| 626 | + | )) | |
| 627 | + | } | |
| 628 | + | Emission::Activate(action) => Err(syn::Error::new( | |
| 629 | + | action.verb.span(), | |
| 630 | + | "`activate` says what opening a row or a cell does; it is not a node", | |
| 631 | + | )), | |
| 572 | 632 | Emission::Beside { priority, .. } => Err(syn::Error::new( | |
| 573 | 633 | priority.span(), | |
| 574 | 634 | "`beside` needs a run in scope, which is rule R3", | |
| @@ -602,10 +662,16 @@ | |||
| 602 | 662 | let mut fields = Vec::new(); | |
| 603 | 663 | for item in body { | |
| 604 | 664 | match item { | |
| 605 | - | Item::Attribute { name, args } if name == "submit" => { | |
| 665 | + | Item::Attribute { name, args, guard } if name == "submit" => { | |
| 606 | 666 | let [label] = args.as_slice() else { | |
| 607 | 667 | return Err(syn::Error::new(name.span(), "`submit` says one thing")); | |
| 608 | 668 | }; | |
| 669 | + | if let Some(guard) = guard { | |
| 670 | + | return Err(syn::Error::new( | |
| 671 | + | guard.span, | |
| 672 | + | "a form always has a button: `submit` takes no guard", | |
| 673 | + | )); | |
| 674 | + | } | |
| 609 | 675 | submit = Some(arg(label)?); | |
| 610 | 676 | } | |
| 611 | 677 | Item::Emit(Emission::Field { | |
| @@ -711,6 +777,51 @@ | |||
| 711 | 777 | .collect() | |
| 712 | 778 | } | |
| 713 | 779 | ||
| 780 | + | /// One table: its columns, its rows, and what it has not shown. | |
| 781 | + | /// | |
| 782 | + | /// Both halves accrete. `Table::new` takes its columns as a list and the form | |
| 783 | + | /// has no expression to hold one in, so `Table::column` was added beside it | |
| 784 | + | /// rather than the form growing a production that admits a list of built | |
| 785 | + | /// values. Columns before rows is the vocabulary's own rule and this emits them | |
| 786 | + | /// in the order they were written, so a declaration that gets it wrong trips | |
| 787 | + | /// `Table::row`'s assertion rather than losing a cell quietly. | |
| 788 | + | fn table(items: &[Item]) -> Result<TokenStream> { | |
| 789 | + | accrete( | |
| 790 | + | items, | |
| 791 | + | Container::Table, | |
| 792 | + | "e!(::quasi_router::screen::Table::new(::std::iter::empty())), | |
| 793 | + | ) | |
| 794 | + | } | |
| 795 | + | ||
| 796 | + | /// One column: what it is called, and how it narrows. | |
| 797 | + | fn column(name: &Arg, body: &[Item]) -> Result<TokenStream> { | |
| 798 | + | let name = arg(name)?; | |
| 799 | + | accrete( | |
| 800 | + | body, | |
| 801 | + | Container::Column, | |
| 802 | + | "e!(::quasi_router::screen::Column::new(#name)), | |
| 803 | + | ) | |
| 804 | + | } | |
| 805 | + | ||
| 806 | + | /// One row of a table, by position. | |
| 807 | + | fn cells(body: &[Item]) -> Result<TokenStream> { | |
| 808 | + | accrete( | |
| 809 | + | body, | |
| 810 | + | Container::Cells, | |
| 811 | + | "e!(<::quasi_router::screen::Cells as ::std::default::Default>::default()), | |
| 812 | + | ) | |
| 813 | + | } | |
| 814 | + | ||
| 815 | + | /// One cell: what it says, and what opening it does. | |
| 816 | + | fn cell(value: &Arg, body: &[Item]) -> Result<TokenStream> { | |
| 817 | + | let value = arg(value)?; | |
| 818 | + | accrete( | |
| 819 | + | body, | |
| 820 | + | Container::Cell, | |
| 821 | + | "e!(::quasi_router::screen::Cell::new(#value)), | |
| 822 | + | ) | |
| 823 | + | } | |
| 824 | + | ||
| 714 | 825 | /// One row of a list: what it says, and what it offers. | |
| 715 | 826 | fn row(primary: &Arg, body: &[Item]) -> Result<TokenStream> { | |
| 716 | 827 | let primary = arg(primary)?; | |
| @@ -942,5 +1053,8 @@ | |||
| 942 | 1053 | Emission::Region { kind, .. } => kind.span(), | |
| 943 | 1054 | Emission::Across { fallback, .. } => fallback.span(), | |
| 944 | 1055 | Emission::Beside { priority, .. } => priority.span(), | |
| 1056 | + | Emission::Table(_) | Emission::Cells(_) => Span::call_site(), | |
| 1057 | + | Emission::Column { .. } | Emission::Cell { .. } => Span::call_site(), | |
| 1058 | + | Emission::Activate(action) => action.verb.span(), | |
| 945 | 1059 | } | |
| 946 | 1060 | } |
| @@ -42,8 +42,8 @@ | |||
| 42 | 42 | /// without a production is a parse error naming it, which is the failure worth | |
| 43 | 43 | /// having. | |
| 44 | 44 | const MEMBERS: &[&str] = &[ | |
| 45 | - | "act", "across", "beside", "field", "form", "given", "include", "link", "region", "row", | |
| 46 | - | "screen", | |
| 45 | + | "act", "across", "activate", "beside", "cell", "cells", "column", "field", "form", "given", | |
| 46 | + | "include", "link", "region", "row", "screen", "table", | |
| 47 | 47 | ]; | |
| 48 | 48 | ||
| 49 | 49 | /// The members that are one of `Node`'s own constructors, called by its name. | |
| @@ -57,6 +57,7 @@ | |||
| 57 | 57 | "stats", | |
| 58 | 58 | "banner", | |
| 59 | 59 | "failed", | |
| 60 | + | "empty", | |
| 60 | 61 | ]; | |
| 61 | 62 | ||
| 62 | 63 | /// The arrangements a screen may be laid out in. | |
| @@ -74,7 +75,11 @@ | |||
| 74 | 75 | ||
| 75 | 76 | /// The attributes whose argument is a variant of a vocabulary enum, and the | |
| 76 | 77 | /// enum it belongs to. Rule R1(4). | |
| 77 | - | pub const VOCABULARY: &[(&str, &str)] = &[("tone", "Tone")]; | |
| 78 | + | pub const VOCABULARY: &[(&str, &str)] = &[ | |
| 79 | + | ("tone", "Tone"), | |
| 80 | + | ("width", "Width"), | |
| 81 | + | ("priority", "Priority"), | |
| 82 | + | ]; | |
| 78 | 83 | ||
| 79 | 84 | /// The attributes the generated function may carry. | |
| 80 | 85 | const FLAGS: &[&str] = &["must_use", "inline"]; | |
| @@ -217,11 +222,12 @@ | |||
| 217 | 222 | } | |
| 218 | 223 | let name: Ident = input.parse()?; | |
| 219 | 224 | let mut args = Vec::new(); | |
| 220 | - | while !input.peek(Token![;]) { | |
| 225 | + | while !input.peek(Token![;]) && guard_ahead(input)?.is_none() { | |
| 221 | 226 | args.push(input.parse()?); | |
| 222 | 227 | } | |
| 228 | + | let guard = guard(input)?; | |
| 223 | 229 | input.parse::<Token![;]>()?; | |
| 224 | - | Ok(Self::Attribute { name, args }) | |
| 230 | + | Ok(Self::Attribute { name, args, guard }) | |
| 225 | 231 | } | |
| 226 | 232 | } | |
| 227 | 233 | ||
| @@ -486,6 +492,35 @@ | |||
| 486 | 492 | Ok(Self::Row { primary, body }) | |
| 487 | 493 | } | |
| 488 | 494 | "list" if input.peek(token::Brace) => Ok(Self::List(block(input)?)), | |
| 495 | + | "table" => Ok(Self::Table(block(input)?)), | |
| 496 | + | "column" => { | |
| 497 | + | let name: Arg = input.parse()?; | |
| 498 | + | let body = if input.peek(token::Brace) { | |
| 499 | + | block(input)? | |
| 500 | + | } else { | |
| 501 | + | input.parse::<Token![;]>()?; | |
| 502 | + | Vec::new() | |
| 503 | + | }; | |
| 504 | + | Ok(Self::Column { name, body }) | |
| 505 | + | } | |
| 506 | + | "cells" => Ok(Self::Cells(block(input)?)), | |
| 507 | + | "cell" => { | |
| 508 | + | let value: Arg = input.parse()?; | |
| 509 | + | let body = if input.peek(token::Brace) { | |
| 510 | + | block(input)? | |
| 511 | + | } else { | |
| 512 | + | input.parse::<Token![;]>()?; | |
| 513 | + | Vec::new() | |
| 514 | + | }; | |
| 515 | + | Ok(Self::Cell { value, body }) | |
| 516 | + | } | |
| 517 | + | "activate" => { | |
| 518 | + | preposition(input, "to")?; | |
| 519 | + | let action: Action = input.parse()?; | |
| 520 | + | let guard = guard(input)?; | |
| 521 | + | input.parse::<Token![;]>()?; | |
| 522 | + | Ok(guarded(guard, Self::Activate(action))) | |
| 523 | + | } | |
| 489 | 524 | "form" => { | |
| 490 | 525 | let action: Action = input.parse()?; | |
| 491 | 526 | Ok(Self::Form { | |
| @@ -576,10 +611,7 @@ | |||
| 576 | 611 | return Ok(first); | |
| 577 | 612 | }; | |
| 578 | 613 | let mut clauses = vec![first]; | |
| 579 | - | loop { | |
| 580 | - | let Some(next) = connective_ahead(input)? else { | |
| 581 | - | break; | |
| 582 | - | }; | |
| 614 | + | while let Some(next) = connective_ahead(input)? { | |
| 583 | 615 | if next != connective { | |
| 584 | 616 | return Err(syn::Error::new( | |
| 585 | 617 | input.span(), |
| @@ -7324,6 +7324,20 @@ | |||
| 7324 | 7324 | self | |
| 7325 | 7325 | } | |
| 7326 | 7326 | ||
| 7327 | + | /// One cell, by position, appended after the ones already there. | |
| 7328 | + | /// | |
| 7329 | + | /// The accreting half of [`new`](Self::new), for the same reason | |
| 7330 | + | /// [`Table::column`] is the accreting half of [`Table::new`]: a caller with | |
| 7331 | + | /// no expression to hold a list in still has to be able to say a row of | |
| 7332 | + | /// cells. Positional, so it pairs with [`Table::column`] and not with | |
| 7333 | + | /// [`at`](Self::at); mixing the two in one row is what `at`'s own docs warn | |
| 7334 | + | /// against. | |
| 7335 | + | #[must_use] | |
| 7336 | + | pub fn cell(mut self, cell: impl Into<Cell>) -> Self { | |
| 7337 | + | self.values.push(cell.into()); | |
| 7338 | + | self | |
| 7339 | + | } | |
| 7340 | + | ||
| 7327 | 7341 | /// This is the row being shown elsewhere. | |
| 7328 | 7342 | /// | |
| 7329 | 7343 | /// Was reachable only by assigning the field, which is why a row built by | |
| @@ -7502,6 +7516,26 @@ | |||
| 7502 | 7516 | } | |
| 7503 | 7517 | } | |
| 7504 | 7518 | ||
| 7519 | + | /// Add one column, for a caller building the table a piece at a time. | |
| 7520 | + | /// | |
| 7521 | + | /// Beside [`new`](Self::new) rather than instead of it: a table whose | |
| 7522 | + | /// columns are a literal list still says so in one expression, and a table | |
| 7523 | + | /// assembled by something that accretes (the declared form, which has no | |
| 7524 | + | /// expression to hold a list in) says the same thing one column at a time. | |
| 7525 | + | /// | |
| 7526 | + | /// **Columns before rows.** [`row`](Self::row) resolves a named cell | |
| 7527 | + | /// against the columns the table has when the row arrives, so a column | |
| 7528 | + | /// added afterwards is invisible to every row already pushed. | |
| 7529 | + | #[must_use] | |
| 7530 | + | pub fn column(mut self, column: Column) -> Self { | |
| 7531 | + | debug_assert!( | |
| 7532 | + | self.rows.is_empty(), | |
| 7533 | + | "a column added after a row cannot be seen by that row's named cells" | |
| 7534 | + | ); | |
| 7535 | + | self.columns.push(column); | |
| 7536 | + | self | |
| 7537 | + | } | |
| 7538 | + | ||
| 7505 | 7539 | /// Add a row, resolving any cell that named its column. | |
| 7506 | 7540 | /// | |
| 7507 | 7541 | /// A named cell whose column this table does not have is dropped: the |