max / quasi
- Record
- wiki quasi-declare-form section 13.
5 files changed,
+246 insertions,
-31 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | 1 | [package] | |
| 2 | 2 | name = "quasi-router" | |
| 3 | - | version = "0.101.5" | |
| 3 | + | version = "0.101.6" | |
| 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 |
| @@ -150,8 +150,17 @@ | |||
| 150 | 150 | ||
| 151 | 151 | /// Everything that emits into the enclosing container. | |
| 152 | 152 | pub enum Emission { | |
| 153 | - | /// `<member> <arg>*;` -- one of `Node`'s own constructors, by its own name. | |
| 154 | - | Simple { member: Ident, args: Vec<Arg> }, | |
| 153 | + | /// `<member> <arg>* ( "{" { item } "}" | ";" )` -- one of `Node`'s own | |
| 154 | + | /// constructors, by its own name, and whatever it is told afterwards. | |
| 155 | + | /// | |
| 156 | + | /// The body is what `forum_memberships` demanded: `Node::empty` carries a | |
| 157 | + | /// way out (`offering`) on the library's pane and not on the settings pane, | |
| 158 | + | /// so an empty state is a member with a setting rather than a bare one. | |
| 159 | + | Simple { | |
| 160 | + | member: Ident, | |
| 161 | + | args: Vec<Arg>, | |
| 162 | + | body: Vec<Item>, | |
| 163 | + | }, | |
| 155 | 164 | /// `screen <arrangement> <arg> { .. }` | |
| 156 | 165 | Screen { | |
| 157 | 166 | arrangement: Ident, | |
| @@ -190,14 +199,36 @@ | |||
| 190 | 199 | Table(Vec<Item>), | |
| 191 | 200 | /// `column <name> ( "{" .. "}" | ";" )` -- one column and how it narrows. | |
| 192 | 201 | Column { name: Arg, body: Vec<Item> }, | |
| 193 | - | /// `cells { cell ..; }` -- one row of a table, by position. | |
| 202 | + | /// `cells { cell ..; }` -- one row of a table. | |
| 194 | 203 | /// | |
| 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. | |
| 204 | + | /// A row's cells are all positional or all named; mixing the two is what | |
| 205 | + | /// `Cells::at`'s own docs warn against. | |
| 198 | 206 | Cells(Vec<Item>), | |
| 199 | - | /// `cell <value> ( "{" .. "}" | ";" )` | |
| 200 | - | Cell { value: Arg, body: Vec<Item> }, | |
| 207 | + | /// `cell [ "at" <column> ] <value> ( "{" .. "}" | ";" )` | |
| 208 | + | /// | |
| 209 | + | /// Positional without the column, named with it. `item_sales` demanded the | |
| 210 | + | /// named form: its columns are module consts and its cells are a function | |
| 211 | + | /// away, so a row written by position would be lined up against headings | |
| 212 | + | /// nobody reading the row can see. Naming is `Cells::at`, and the two are | |
| 213 | + | /// not mixed in one row -- that warning is `at`'s own. | |
| 214 | + | Cell { | |
| 215 | + | column: Option<Arg>, | |
| 216 | + | value: Arg, | |
| 217 | + | body: Vec<Item>, | |
| 218 | + | }, | |
| 219 | + | /// `offering <arg> to <action> ( "{" { item } "}" | ";" )` -- the way out an | |
| 220 | + | /// empty or failed state offers. | |
| 221 | + | /// | |
| 222 | + | /// Spelled like `act` and placed like a setting, because `Node::offering` | |
| 223 | + | /// takes an `Act` and a setting's arguments are args. Its own member rather | |
| 224 | + | /// than an `act` the container reinterprets: a body holding a control and a | |
| 225 | + | /// body holding a way out would otherwise read the same and mean different | |
| 226 | + | /// things. | |
| 227 | + | Offering { | |
| 228 | + | label: Arg, | |
| 229 | + | action: Action, | |
| 230 | + | body: Vec<Item>, | |
| 231 | + | }, | |
| 201 | 232 | /// `activate to <action>;` -- what opening this row or cell does. | |
| 202 | 233 | /// | |
| 203 | 234 | /// A setting in shape, and a member in the tree, because the thing it sets | |
| @@ -211,7 +242,7 @@ | |||
| 211 | 242 | /// `region <arg> as <kind> { .. }` | |
| 212 | 243 | Region { | |
| 213 | 244 | name: Arg, | |
| 214 | - | kind: Ident, | |
| 245 | + | kind: RegionKind, | |
| 215 | 246 | body: Vec<Item>, | |
| 216 | 247 | }, | |
| 217 | 248 | /// `across <fallback> { .. }` | |
| @@ -239,6 +270,20 @@ | |||
| 239 | 270 | }, | |
| 240 | 271 | } | |
| 241 | 272 | ||
| 273 | + | /// What kind of region this is: a variant, or a hole that answers with one. | |
| 274 | + | /// | |
| 275 | + | /// `RegionKind::Widget` carries a name, so it is the one kind a bare ident | |
| 276 | + | /// cannot spell. `media_picker` has three widget regions and a `widget(name)` | |
| 277 | + | /// helper beside them, which is the deferred table's own remedy for a value the | |
| 278 | + | /// form cannot say; this lets the declaration call it. | |
| 279 | + | /// | |
| 280 | + | /// Told apart by R1's rule and not by a keyword: a bare uppercase ident is a | |
| 281 | + | /// variant, and anything else is a hole. | |
| 282 | + | pub enum RegionKind { | |
| 283 | + | Variant(Ident), | |
| 284 | + | Supplied(Hole), | |
| 285 | + | } | |
| 286 | + | ||
| 242 | 287 | /// `when <predicate>`, or `unless <predicate>`, which is its negation. | |
| 243 | 288 | pub struct Guard { | |
| 244 | 289 | pub negated: bool, |
| @@ -17,7 +17,7 @@ | |||
| 17 | 17 | ||
| 18 | 18 | use crate::ast::{ | |
| 19 | 19 | Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Pattern, | |
| 20 | - | Predicate, Source, Step, StrPart, | |
| 20 | + | Predicate, RegionKind, Source, Step, StrPart, | |
| 21 | 21 | }; | |
| 22 | 22 | use crate::parse::{COMPARISONS, VOCABULARY}; | |
| 23 | 23 | ||
| @@ -44,6 +44,8 @@ | |||
| 44 | 44 | Cells, | |
| 45 | 45 | /// One cell: its body says what opening it does. | |
| 46 | 46 | Cell, | |
| 47 | + | /// One node member: its body says what it is told afterwards. | |
| 48 | + | Node, | |
| 47 | 49 | } | |
| 48 | 50 | ||
| 49 | 51 | pub fn declaration(declaration: &Declaration) -> Result<TokenStream> { | |
| @@ -227,6 +229,29 @@ | |||
| 227 | 229 | )); | |
| 228 | 230 | } | |
| 229 | 231 | }, | |
| 232 | + | "Cells" => match only { | |
| 233 | + | Emission::Cells(body) => cells(body)?, | |
| 234 | + | other => { | |
| 235 | + | return Err(syn::Error::new( | |
| 236 | + | emission_span(other), | |
| 237 | + | "a `-> Cells` shape is its single `cells` member", | |
| 238 | + | )); | |
| 239 | + | } | |
| 240 | + | }, | |
| 241 | + | "Field" => match only { | |
| 242 | + | Emission::Field { | |
| 243 | + | kind, | |
| 244 | + | name, | |
| 245 | + | label, | |
| 246 | + | body, | |
| 247 | + | } => field(kind, name, label, body)?, | |
| 248 | + | other => { | |
| 249 | + | return Err(syn::Error::new( | |
| 250 | + | emission_span(other), | |
| 251 | + | "a `-> Field` shape is its single `field` member", | |
| 252 | + | )); | |
| 253 | + | } | |
| 254 | + | }, | |
| 230 | 255 | other => { | |
| 231 | 256 | return Err(syn::Error::new( | |
| 232 | 257 | span, | |
| @@ -459,20 +484,68 @@ | |||
| 459 | 484 | Ok((column(name, body)?, quote!(.column(#held)))) | |
| 460 | 485 | } | |
| 461 | 486 | (Container::Table, Emission::Cells(body)) => Ok((cells(body)?, quote!(.row(#held)))), | |
| 487 | + | // A row another shape built. `item_sales` splits its columns from its | |
| 488 | + | // cells on purpose, so the row arrives whole rather than being written | |
| 489 | + | // inside the table. | |
| 490 | + | (Container::Table, Emission::Include(supplier)) => { | |
| 491 | + | let supplier = hole(supplier)?; | |
| 492 | + | Ok(( | |
| 493 | + | quote!(::std::convert::Into::into(#supplier)), | |
| 494 | + | quote!(.row(#held)), | |
| 495 | + | )) | |
| 496 | + | } | |
| 462 | 497 | (Container::Table, other) => Err(syn::Error::new( | |
| 463 | 498 | emission_span(other), | |
| 464 | - | "a table holds columns and rows: write `column <name>;` or `cells { .. }`", | |
| 499 | + | "a table holds columns and rows: write `column <name>;`, `cells { .. }` \ | |
| 500 | + | or `include <shape>;`", | |
| 465 | 501 | )), | |
| 466 | - | (Container::Cells, Emission::Cell { value, body }) => { | |
| 467 | - | Ok((cell(value, body)?, quote!(.cell(#held)))) | |
| 502 | + | ( | |
| 503 | + | Container::Cells, | |
| 504 | + | Emission::Cell { | |
| 505 | + | column, | |
| 506 | + | value, | |
| 507 | + | body, | |
| 508 | + | }, | |
| 509 | + | ) => { | |
| 510 | + | let built = cell(value, body)?; | |
| 511 | + | Ok(match column { | |
| 512 | + | Some(column) => { | |
| 513 | + | let column = arg(column)?; | |
| 514 | + | (built, quote!(.at(#column, #held))) | |
| 515 | + | } | |
| 516 | + | None => (built, quote!(.cell(#held))), | |
| 517 | + | }) | |
| 468 | 518 | } | |
| 469 | 519 | (Container::Cells, other) => Err(syn::Error::new( | |
| 470 | 520 | emission_span(other), | |
| 471 | 521 | "a row of a table holds cells: write `cell <value>;`", | |
| 472 | 522 | )), | |
| 523 | + | (Container::Cell, Emission::Act { .. }) => Ok((act(emission)?, quote!(.act(#held)))), | |
| 473 | 524 | (Container::Cell, other) => Err(syn::Error::new( | |
| 474 | 525 | emission_span(other), | |
| 475 | - | "a cell says what opening it does, and nothing else yet", | |
| 526 | + | "a cell holds controls and says what opening it does: write \ | |
| 527 | + | `act <label> to <action>;` or `activate to <action>;`", | |
| 528 | + | )), | |
| 529 | + | ( | |
| 530 | + | Container::Node, | |
| 531 | + | Emission::Offering { | |
| 532 | + | label, | |
| 533 | + | action, | |
| 534 | + | body, | |
| 535 | + | }, | |
| 536 | + | ) => { | |
| 537 | + | let label = arg(label)?; | |
| 538 | + | let called = self::action(action)?; | |
| 539 | + | let way_out = accrete( | |
| 540 | + | body, | |
| 541 | + | Container::Act, | |
| 542 | + | "e!(::quasi_router::Act::new(#label, #called)), | |
| 543 | + | )?; | |
| 544 | + | Ok((way_out, quote!(.offering(#held)))) | |
| 545 | + | } | |
| 546 | + | (Container::Node, other) => Err(syn::Error::new( | |
| 547 | + | emission_span(other), | |
| 548 | + | "a node member is told settings and a way out: write `offering <label> to <action>;`", | |
| 476 | 549 | )), | |
| 477 | 550 | (Container::Column, other) => Err(syn::Error::new( | |
| 478 | 551 | emission_span(other), | |
| @@ -543,9 +616,13 @@ | |||
| 543 | 616 | /// One emission as a `Node` value. | |
| 544 | 617 | fn node(emission: &Emission) -> Result<TokenStream> { | |
| 545 | 618 | match emission { | |
| 546 | - | Emission::Simple { member, args } => { | |
| 619 | + | Emission::Simple { member, args, body } => { | |
| 547 | 620 | let args = args.iter().map(self::arg).collect::<Result<Vec<_>>>()?; | |
| 548 | - | Ok(quote!(::quasi_router::Node::#member(#(#args),*))) | |
| 621 | + | accrete( | |
| 622 | + | body, | |
| 623 | + | Container::Node, | |
| 624 | + | "e!(::quasi_router::Node::#member(#(#args),*)), | |
| 625 | + | ) | |
| 549 | 626 | } | |
| 550 | 627 | Emission::List(items) => list(items), | |
| 551 | 628 | Emission::Table(items) => { | |
| @@ -555,7 +632,7 @@ | |||
| 555 | 632 | Emission::Form { action, body } => form(action, body), | |
| 556 | 633 | Emission::Field { kind, .. } => Err(syn::Error::new( | |
| 557 | 634 | kind.span(), | |
| 558 | - | "a field is not a node: put it in a `form`", | |
| 635 | + | "a field is not a node: put it in a `form`, or give the shape `-> Field`", | |
| 559 | 636 | )), | |
| 560 | 637 | Emission::Row { .. } => Err(syn::Error::new( | |
| 561 | 638 | emission_span(emission), | |
| @@ -629,6 +706,10 @@ | |||
| 629 | 706 | action.verb.span(), | |
| 630 | 707 | "`activate` says what opening a row or a cell does; it is not a node", | |
| 631 | 708 | )), | |
| 709 | + | Emission::Offering { action, .. } => Err(syn::Error::new( | |
| 710 | + | action.verb.span(), | |
| 711 | + | "`offering` is the way out an empty state offers; it is not a node", | |
| 712 | + | )), | |
| 632 | 713 | Emission::Beside { priority, .. } => Err(syn::Error::new( | |
| 633 | 714 | priority.span(), | |
| 634 | 715 | "`beside` needs a run in scope, which is rule R3", | |
| @@ -638,12 +719,16 @@ | |||
| 638 | 719 | ||
| 639 | 720 | /// One region, as the `Slot` it is. A document holds these; a body wraps them | |
| 640 | 721 | /// in [`Node::Region`]. | |
| 641 | - | fn slot(name: &Arg, kind: &syn::Ident, body: &[Item]) -> Result<TokenStream> { | |
| 722 | + | fn slot(name: &Arg, kind: &RegionKind, body: &[Item]) -> Result<TokenStream> { | |
| 642 | 723 | let name = arg(name)?; | |
| 724 | + | let kind = match kind { | |
| 725 | + | RegionKind::Variant(variant) => quote!(::quasi_router::RegionKind::#variant), | |
| 726 | + | RegionKind::Supplied(hole) => self::hole(hole)?, | |
| 727 | + | }; | |
| 643 | 728 | accrete( | |
| 644 | 729 | body, | |
| 645 | 730 | Container::Slot, | |
| 646 | - | "e!(::quasi_router::Slot::new(#name, ::quasi_router::RegionKind::#kind)), | |
| 731 | + | "e!(::quasi_router::Slot::new(#name, #kind)), | |
| 647 | 732 | ) | |
| 648 | 733 | } | |
| 649 | 734 | ||
| @@ -812,7 +897,7 @@ | |||
| 812 | 897 | ) | |
| 813 | 898 | } | |
| 814 | 899 | ||
| 815 | - | /// One cell: what it says, and what opening it does. | |
| 900 | + | /// One cell: what it says, what it holds, and what opening it does. | |
| 816 | 901 | fn cell(value: &Arg, body: &[Item]) -> Result<TokenStream> { | |
| 817 | 902 | let value = arg(value)?; | |
| 818 | 903 | accrete( | |
| @@ -1050,11 +1135,15 @@ | |||
| 1050 | 1135 | Emission::Act { action, .. } => action.verb.span(), | |
| 1051 | 1136 | Emission::Guarded { guard, .. } => guard.span, | |
| 1052 | 1137 | Emission::Given { .. } => Span::call_site(), | |
| 1053 | - | Emission::Region { kind, .. } => kind.span(), | |
| 1138 | + | Emission::Region { kind, .. } => match kind { | |
| 1139 | + | RegionKind::Variant(variant) => variant.span(), | |
| 1140 | + | RegionKind::Supplied(_) => Span::call_site(), | |
| 1141 | + | }, | |
| 1054 | 1142 | Emission::Across { fallback, .. } => fallback.span(), | |
| 1055 | 1143 | Emission::Beside { priority, .. } => priority.span(), | |
| 1056 | 1144 | Emission::Table(_) | Emission::Cells(_) => Span::call_site(), | |
| 1057 | 1145 | Emission::Column { .. } | Emission::Cell { .. } => Span::call_site(), | |
| 1058 | 1146 | Emission::Activate(action) => action.verb.span(), | |
| 1147 | + | Emission::Offering { action, .. } => action.verb.span(), | |
| 1059 | 1148 | } | |
| 1060 | 1149 | } |
| @@ -17,10 +17,14 @@ | |||
| 17 | 17 | ||
| 18 | 18 | use crate::ast::{ | |
| 19 | 19 | Action, Arg, Declaration, Emission, Guard, Hole, HoleRoot, Interpolated, Item, Modifier, Param, | |
| 20 | - | Pattern, Predicate, Source, Step, StrPart, | |
| 20 | + | Pattern, Predicate, RegionKind, Source, Step, StrPart, | |
| 21 | 21 | }; | |
| 22 | 22 | ||
| 23 | 23 | /// The modifiers, and how many arguments each takes. | |
| 24 | + | /// | |
| 25 | + | /// `filling` is deliberately not here. It is `Act::filling`, a setting on the | |
| 26 | + | /// control, and listing it as a modifier of the action made `media_picker`'s | |
| 27 | + | /// tiles fail against `Action` instead of reading as the setting they are. | |
| 24 | 28 | const MODIFIERS: &[(&str, usize)] = &[ | |
| 25 | 29 | ("navigating", 0), | |
| 26 | 30 | ("awaiting", 0), | |
| @@ -31,7 +35,6 @@ | |||
| 31 | 35 | ("saving", 1), | |
| 32 | 36 | ("replacing", 1), | |
| 33 | 37 | ("copying", 1), | |
| 34 | - | ("filling", 2), | |
| 35 | 38 | ("carrying", 2), | |
| 36 | 39 | ]; | |
| 37 | 40 | ||
| @@ -43,7 +46,7 @@ | |||
| 43 | 46 | /// having. | |
| 44 | 47 | const MEMBERS: &[&str] = &[ | |
| 45 | 48 | "act", "across", "activate", "beside", "cell", "cells", "column", "field", "form", "given", | |
| 46 | - | "include", "link", "region", "row", "screen", "table", | |
| 49 | + | "include", "link", "offering", "region", "row", "screen", "table", | |
| 47 | 50 | ]; | |
| 48 | 51 | ||
| 49 | 52 | /// The members that are one of `Node`'s own constructors, called by its name. | |
| @@ -58,6 +61,7 @@ | |||
| 58 | 61 | "banner", | |
| 59 | 62 | "failed", | |
| 60 | 63 | "empty", | |
| 64 | + | "rich", | |
| 61 | 65 | ]; | |
| 62 | 66 | ||
| 63 | 67 | /// The arrangements a screen may be laid out in. | |
| @@ -288,6 +292,22 @@ | |||
| 288 | 292 | }) | |
| 289 | 293 | } | |
| 290 | 294 | ||
| 295 | + | impl Parse for RegionKind { | |
| 296 | + | fn parse(input: ParseStream) -> Result<Self> { | |
| 297 | + | // R1's rule, applied to a kind: a bare uppercase ident is the variant, | |
| 298 | + | // and anything else is a hole that answers with one. | |
| 299 | + | if input.peek(Ident) && !input.peek2(Token![::]) && !input.peek2(token::Paren) { | |
| 300 | + | let name: Ident = input.fork().parse()?; | |
| 301 | + | let text = name.to_string(); | |
| 302 | + | if text.starts_with(|letter: char| letter.is_uppercase()) { | |
| 303 | + | input.parse::<Ident>()?; | |
| 304 | + | return Ok(Self::Variant(name)); | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | Ok(Self::Supplied(input.parse()?)) | |
| 308 | + | } | |
| 309 | + | } | |
| 310 | + | ||
| 291 | 311 | impl Parse for Pattern { | |
| 292 | 312 | fn parse(input: ParseStream) -> Result<Self> { | |
| 293 | 313 | if input.peek(LitInt) { | |
| @@ -403,10 +423,9 @@ | |||
| 403 | 423 | "region" => { | |
| 404 | 424 | let name: Arg = input.parse()?; | |
| 405 | 425 | input.parse::<Token![as]>()?; | |
| 406 | - | let kind: Ident = input.parse()?; | |
| 407 | 426 | Ok(Self::Region { | |
| 408 | 427 | name, | |
| 409 | - | kind, | |
| 428 | + | kind: input.parse()?, | |
| 410 | 429 | body: block(input)?, | |
| 411 | 430 | }) | |
| 412 | 431 | } | |
| @@ -505,6 +524,12 @@ | |||
| 505 | 524 | } | |
| 506 | 525 | "cells" => Ok(Self::Cells(block(input)?)), | |
| 507 | 526 | "cell" => { | |
| 527 | + | let column = if input.peek(Ident) && input.fork().parse::<Ident>()? == "at" { | |
| 528 | + | input.parse::<Ident>()?; | |
| 529 | + | Some(input.parse()?) | |
| 530 | + | } else { | |
| 531 | + | None | |
| 532 | + | }; | |
| 508 | 533 | let value: Arg = input.parse()?; | |
| 509 | 534 | let body = if input.peek(token::Brace) { | |
| 510 | 535 | block(input)? | |
| @@ -512,7 +537,31 @@ | |||
| 512 | 537 | input.parse::<Token![;]>()?; | |
| 513 | 538 | Vec::new() | |
| 514 | 539 | }; | |
| 515 | - | Ok(Self::Cell { value, body }) | |
| 540 | + | Ok(Self::Cell { | |
| 541 | + | column, | |
| 542 | + | value, | |
| 543 | + | body, | |
| 544 | + | }) | |
| 545 | + | } | |
| 546 | + | "offering" => { | |
| 547 | + | let label: Arg = input.parse()?; | |
| 548 | + | preposition(input, "to")?; | |
| 549 | + | let action: Action = input.parse()?; | |
| 550 | + | let guard = guard(input)?; | |
| 551 | + | let body = if input.peek(token::Brace) { | |
| 552 | + | block(input)? | |
| 553 | + | } else { | |
| 554 | + | input.parse::<Token![;]>()?; | |
| 555 | + | Vec::new() | |
| 556 | + | }; | |
| 557 | + | Ok(guarded( | |
| 558 | + | guard, | |
| 559 | + | Self::Offering { | |
| 560 | + | label, | |
| 561 | + | action, | |
| 562 | + | body, | |
| 563 | + | }, | |
| 564 | + | )) | |
| 516 | 565 | } | |
| 517 | 566 | "activate" => { | |
| 518 | 567 | preposition(input, "to")?; | |
| @@ -547,16 +596,28 @@ | |||
| 547 | 596 | } | |
| 548 | 597 | name if NODE_MEMBERS.contains(&name) => { | |
| 549 | 598 | let mut args = Vec::new(); | |
| 550 | - | while !input.peek(Token![;]) && guard_ahead(input)?.is_none() { | |
| 599 | + | while !input.peek(Token![;]) | |
| 600 | + | && !input.peek(token::Brace) | |
| 601 | + | && guard_ahead(input)?.is_none() | |
| 602 | + | { | |
| 551 | 603 | args.push(input.parse()?); | |
| 552 | 604 | } | |
| 553 | 605 | let guard = guard(input)?; | |
| 554 | - | input.parse::<Token![;]>()?; | |
| 606 | + | // Told something afterwards, or nothing. `act` reads the same | |
| 607 | + | // way and for the same reason: a member with nothing to say | |
| 608 | + | // about itself should not have to open a block to say so. | |
| 609 | + | let body = if input.peek(token::Brace) { | |
| 610 | + | block(input)? | |
| 611 | + | } else { | |
| 612 | + | input.parse::<Token![;]>()?; | |
| 613 | + | Vec::new() | |
| 614 | + | }; | |
| 555 | 615 | Ok(guarded( | |
| 556 | 616 | guard, | |
| 557 | 617 | Self::Simple { | |
| 558 | 618 | member: member.clone(), | |
| 559 | 619 | args, | |
| 620 | + | body, | |
| 560 | 621 | }, | |
| 561 | 622 | )) | |
| 562 | 623 | } | |
| @@ -722,7 +783,11 @@ | |||
| 722 | 783 | }; | |
| 723 | 784 | ||
| 724 | 785 | let mut modifiers = Vec::new(); | |
| 725 | - | while input.peek(Ident) { | |
| 786 | + | // A guard ends the action rather than being read as a modifier of it. | |
| 787 | + | // `offering "Browse Communities" to external base unless base.is_empty()` | |
| 788 | + | // is the site: without this the guard word is the next word after a | |
| 789 | + | // target and the action swallows it. | |
| 790 | + | while input.peek(Ident) && guard_ahead(input)?.is_none() { | |
| 726 | 791 | let name: Ident = input.parse()?; | |
| 727 | 792 | let spelling = name.to_string(); | |
| 728 | 793 | let Some((_, arity)) = MODIFIERS.iter().find(|(known, _)| *known == spelling) else { |
| @@ -3541,6 +3541,22 @@ | |||
| 3541 | 3541 | } | |
| 3542 | 3542 | ||
| 3543 | 3543 | /// A select offering the given options. | |
| 3544 | + | /// The options offered, appended to the ones already there. | |
| 3545 | + | /// | |
| 3546 | + | /// The accreting half of [`select`](Self::select) and [`radio`](Self::radio), | |
| 3547 | + | /// which take the whole list as an argument. Beside them for the reason | |
| 3548 | + | /// [`Table::column`] sits beside `Table::new`: a caller with no expression | |
| 3549 | + | /// to hold a list in still has to be able to offer options, and `options` | |
| 3550 | + | /// was reachable only by assigning the field. | |
| 3551 | + | /// | |
| 3552 | + | /// Says nothing about the kind. A kind that offers no options ignores them, | |
| 3553 | + | /// which is what [`layout::FieldKind::offers_options`] already decides. | |
| 3554 | + | #[must_use] | |
| 3555 | + | pub fn options(mut self, options: impl IntoIterator<Item = Choice>) -> Self { | |
| 3556 | + | self.options.extend(options); | |
| 3557 | + | self | |
| 3558 | + | } | |
| 3559 | + | ||
| 3544 | 3560 | pub fn select(name: impl Into<String>, label: impl Into<String>, options: Vec<Choice>) -> Self { | |
| 3545 | 3561 | Self { | |
| 3546 | 3562 | options, |