Skip to main content

max / quasi

Guard a setting: a placing one is a mark, a settling one is two arms The distinction PLACED was groping at, made and acted on. A setting is one of two things and the vocabulary spelled both the same way. A PLACING setting adds markup at a position of its own -- `more` draws a pager after the rows, `figure` accretes onto a strip, `bar` onto a chart, `option` onto a field, and `back`, `forward` and `jumping` each draw their own control inside a pager. Each is a member in all but name. So each counts in its container's index space, `Rest` learns to be marked, and a guard on one is an ordinary mark over the run it places. A SETTLING setting changes the markup the member its own statement produces was going to write anyway: `chosen` is ` selected` inside an `<option>`, `here` and `latched` are classes inside a tag. There is no run of members to cover, so the member is built both ways and a request picks one -- `Fill::Swap` and `Plan::swap`, where arm 0 is the setting made. `derive` needed nothing: it already renders once per arm and reads each off its own cover. All four parked shapes are staged again and the feature that held their tests back is gone. What that buys, beyond the parking: - quasicoherent `c32bb877` is closed. A Select's chosen option was filed as something a compiled loop body could not carry, and the residual now holds it as one `Arms` inside the loop with no `data-unmatched` anywhere -- the sentinel-matching hack went with the derivation that needed it. - `cbb63155` is closed. The numbered pager strip derives, and its readout is the second arm rather than a substitution nothing could measure. - Section 33's `latched` ruling stops being a special case. One real bug found on the way, by a fixture rather than by a reader: marks covering the same members nested backwards, so a guard written inside a loop body came out holding the loop. A container adds its marks innermost first, so of two marks over one run the later is the outer, and the covers are read in reverse to say it.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-08 20:08 UTC
Signed with PGP, not checked
Commit: 8370d3b997dc1680b42e8b2bbd0d391db26b3e72
Parent: 2599059
12 files changed, +307 insertions, -131 deletions
M Cargo.lock +4 -4
@@ -6312,6 +6312,10 @@
6312 6312 "winnow 1.0.4",
6313 6313 ]
6314 6314
6315 + [[patch.unused]]
6316 + name = "synckit-client"
6317 + version = "0.10.0"
6318 +
6315 6319 [[patch.unused]]
6316 6320 name = "kberg"
6317 6321 version = "0.1.0"
@@ -6331,7 +6335,3 @@
6331 6335 [[patch.unused]]
6332 6336 name = "quasi-type"
6333 6337 version = "0.1.3"
6334 -
6335 - [[patch.unused]]
6336 - name = "synckit-client"
6337 - version = "0.10.0"
@@ -24,12 +24,6 @@
24 24 # not carry a templating engine; on, it adds the third column that says whether
25 25 # a staged description reaches a compiled template.
26 26 askama = ["dep:askama"]
27 - # The residual tests for the four shapes that guard a setting. Off, and those
28 - # shapes are unstaged to match, until a guarded setting can be marked -- see
29 - # the note above each of their `declare!` blocks. Restore the flag and the
30 - # `#[staged]` together, so a residual test never outlives the residual it
31 - # tests.
32 - guarded-settings-stage = []
33 27
34 28 [dependencies]
35 29 quasi-router = { path = "../quasi-router", version = "0.109" }
@@ -45,21 +45,7 @@
45 45
46 46 declare! {
47 47 /// A strip of figures, each of which may report a change.
48 - // PARKED, not staged: this shape guards a setting, and a guarded setting is
49 - // the one thing the mark rewrite does not yet cover. A mark covers a run of
50 - // a container's MEMBERS, and a setting varies markup inside the member its
51 - // own statement produces. Two shapes hide under that one sentence:
52 - //
53 - // PLACING settings (`figure`, `bar`, `more`, `back`, `forward`,
54 - // `jumping`) add markup at a position of their own, so they are members
55 - // in all but name and want counting in their container's index space.
56 - //
57 - // SETTLING settings (`chosen`, `here`) change the tag their member
58 - // already writes, so the answer is two arms over that member -- built
59 - // with the setting and without it -- which `given` already compiles to.
60 - //
61 - // Both are specified and neither is built. Until then this shape keeps the
62 - // runtime renderer, which is what an unstaged shape has always done.
48 + #[staged]
63 49 pub(crate) shape strip(cards: &[Card]) -> Node;
64 50
65 51 stats [] {
@@ -71,11 +57,7 @@
71 57 }
72 58 }
73 59
74 - // PARKED with the shape above: these press the staged path, and the shape is
75 - // not staged until a guarded setting can be marked. Restore both together --
76 - // the shape's `#[staged]` and this module -- so a residual test never outlives
77 - // the residual it tests.
78 - #[cfg(all(test, feature = "guarded-settings-stage"))]
60 + #[cfg(test)]
79 61 mod tests {
80 62 use super::*;
81 63 use quasi_http::Serves as _;
@@ -86,6 +68,12 @@
86 68 quasi_webview::stage::derive(&Webview::new(), strip_staged)
87 69 }
88 70
71 + #[test]
72 + fn probe_residual() {
73 + let residual = residual();
74 + eprintln!("{:#?}", residual.ops());
75 + }
76 +
89 77 #[test]
90 78 fn a_guard_inside_a_loop_body_derives() {
91 79 let webview = Webview::new();
@@ -53,21 +53,7 @@
53 53
54 54 declare! {
55 55 /// The theme picker.
56 - // PARKED, not staged: this shape guards a setting, and a guarded setting is
57 - // the one thing the mark rewrite does not yet cover. A mark covers a run of
58 - // a container's MEMBERS, and a setting varies markup inside the member its
59 - // own statement produces. Two shapes hide under that one sentence:
60 - //
61 - // PLACING settings (`figure`, `bar`, `more`, `back`, `forward`,
62 - // `jumping`) add markup at a position of their own, so they are members
63 - // in all but name and want counting in their container's index space.
64 - //
65 - // SETTLING settings (`chosen`, `here`) change the tag their member
66 - // already writes, so the answer is two arms over that member -- built
67 - // with the setting and without it -- which `given` already compiles to.
68 - //
69 - // Both are specified and neither is built. Until then this shape keeps the
70 - // runtime renderer, which is what an unstaged shape has always done.
56 + #[staged]
71 57 pub(crate) shape picker(themes: &[Theme]) -> Node;
72 58
73 59 form put "/api/users/me/console-theme" awaiting {
@@ -82,11 +68,7 @@
82 68 }
83 69 }
84 70
85 - // PARKED with the shape above: these press the staged path, and the shape is
86 - // not staged until a guarded setting can be marked. Restore both together --
87 - // the shape's `#[staged]` and this module -- so a residual test never outlives
88 - // the residual it tests.
89 - #[cfg(all(test, feature = "guarded-settings-stage"))]
71 + #[cfg(test)]
90 72 mod tests {
91 73 use quasi_http::Serves as _;
92 74 use quasi_router::stage::{Op, Plan, Residual};
@@ -57,21 +57,7 @@
57 57
58 58 declare! {
59 59 /// The listing, with the pager that says what it has not shown.
60 - // PARKED, not staged: this shape guards a setting, and a guarded setting is
61 - // the one thing the mark rewrite does not yet cover. A mark covers a run of
62 - // a container's MEMBERS, and a setting varies markup inside the member its
63 - // own statement produces. Two shapes hide under that one sentence:
64 - //
65 - // PLACING settings (`figure`, `bar`, `more`, `back`, `forward`,
66 - // `jumping`) add markup at a position of their own, so they are members
67 - // in all but name and want counting in their container's index space.
68 - //
69 - // SETTLING settings (`chosen`, `here`) change the tag their member
70 - // already writes, so the answer is two arms over that member -- built
71 - // with the setting and without it -- which `given` already compiles to.
72 - //
73 - // Both are specified and neither is built. Until then this shape keeps the
74 - // runtime renderer, which is what an unstaged shape has always done.
60 + #[staged]
75 61 pub(crate) shape listing(loaded: &Loaded) -> Node;
76 62
77 63 table {
@@ -98,11 +84,7 @@
98 84 }
99 85 }
100 86
101 - // PARKED with the shape above: these press the staged path, and the shape is
102 - // not staged until a guarded setting can be marked. Restore both together --
103 - // the shape's `#[staged]` and this module -- so a residual test never outlives
104 - // the residual it tests.
105 - #[cfg(all(test, feature = "guarded-settings-stage"))]
87 + #[cfg(test)]
106 88 mod tests {
107 89 use quasi_http::Serves as _;
108 90 use quasi_router::stage::{Op, Plan, Residual};
@@ -235,21 +217,7 @@
235 217 /// other page is a control, which is a substitution rather than a gap --
236 218 /// `here` places nothing that turning it off would delete. See
237 219 /// `strip_tests` below.
238 - // PARKED, not staged: this shape guards a setting, and a guarded setting is
239 - // the one thing the mark rewrite does not yet cover. A mark covers a run of
240 - // a container's MEMBERS, and a setting varies markup inside the member its
241 - // own statement produces. Two shapes hide under that one sentence:
242 - //
243 - // PLACING settings (`figure`, `bar`, `more`, `back`, `forward`,
244 - // `jumping`) add markup at a position of their own, so they are members
245 - // in all but name and want counting in their container's index space.
246 - //
247 - // SETTLING settings (`chosen`, `here`) change the tag their member
248 - // already writes, so the answer is two arms over that member -- built
249 - // with the setting and without it -- which `given` already compiles to.
250 - //
251 - // Both are specified and neither is built. Until then this shape keeps the
252 - // runtime renderer, which is what an unstaged shape has always done.
220 + #[staged]
253 221 pub(crate) shape windowed(loaded: &Windowed) -> Node;
254 222
255 223 table {
@@ -270,11 +238,7 @@
270 238 }
271 239 }
272 240
273 - // PARKED with the shape above: these press the staged path, and the shape is
274 - // not staged until a guarded setting can be marked. Restore both together --
275 - // the shape's `#[staged]` and this module -- so a residual test never outlives
276 - // the residual it tests.
277 - #[cfg(all(test, feature = "guarded-settings-stage"))]
241 + #[cfg(test)]
278 242 mod strip_tests {
279 243 use quasi_http::Serves as _;
280 244 use quasi_router::stage::{Op, Residual};
@@ -637,14 +637,33 @@
637 637 items
638 638 .iter()
639 639 .map(|item| match item {
640 - // Settling an argument is not accreting members either. A guard on
641 - // a setting inside one of these varies markup INSIDE the member its
642 - // statement produces, so the mark belongs to the container that
643 - // member lands in, which is where the twin puts it.
644 - Item::Marked { .. } => Err(syn::Error::new(
645 - proc_macro2::Span::call_site(),
646 - "a marked run where an argument is settled, which has no members",
647 - )),
640 + // An argument may be a container in its own right: a `Rest` holds
641 + // the controls a pager draws, and `back`, `forward` and `jumping`
642 + // place them. So a mark here covers a run of the ARGUMENT's members
643 + // rather than the enclosing container's, and the three lines are
644 + // the same three, written against the binding being settled.
645 + Item::Marked { site, varies, body } => {
646 + let inner = argument_settings(body, held)?;
647 + let varies = self::varies(varies)?;
648 + let plan = format_ident!("{}", crate::symbolic::PLAN, span = Span::call_site());
649 + let at = format_ident!("settled_from_{}", held, span = Span::call_site());
650 + let to = format_ident!("settled_to_{}", held, span = Span::call_site());
651 + Ok(quote!({
652 + let #at = ::quasi_router::stage::Marking::placed(&#held);
653 + #(#inner)*
654 + let #to = ::quasi_router::stage::Marking::placed(&#held);
655 + ::quasi_router::stage::Marking::mark(
656 + &mut #held,
657 + ::quasi_router::stage::Mark {
658 + scope: #plan.scope(),
659 + id: #site,
660 + varies: #varies,
661 + from: #at,
662 + to: #to,
663 + },
664 + );
665 + }))
666 + }
648 667 Item::Attribute {
649 668 name,
650 669 args,
@@ -238,6 +238,29 @@
238 238 }
239 239 }
240 240 }
241 + // Two markups at one position, picked by a predicate rather than by a
242 + // value. `Arms`'s machinery exactly, with the scrutinee replaced: arm 0
243 + // is the setting made, which is `Plan::swap`'s own reading.
244 + Fill::Swap { guard } => {
245 + let predicate = crate::emit::predicate(guard)?;
246 + let answers = answers(level)?;
247 + quote! {
248 + {
249 + let taken = usize::from(!(#predicate));
250 + ::quasi_router::stage::Cursor::pick(
251 + cursor,
252 + out,
253 + taken,
254 + &mut |which, out| match which {
255 + #(#answers)*
256 + _ => ::core::unreachable!(
257 + "the residual has a hole the filler does not"
258 + ),
259 + },
260 + );
261 + }
262 + }
263 + }
241 264 // A dispatch: the residual holds one arm per position and the request
242 265 // picks the index. No body to walk, because every arm's holes were
243 266 // numbered at this level -- see `symbolic::Fill::Arms`.
@@ -179,6 +179,20 @@
179 179 iterable: Hole,
180 180 body: Vec<Fill>,
181 181 },
182 + /// Two markups at one position, and the predicate that picks between them.
183 + ///
184 + /// What a guarded SETTLING setting compiles to. `chosen`, `here` and
185 + /// `latched` change the markup the member their own statement produces was
186 + /// going to write anyway, so there is no run of members for a branch to
187 + /// cover; the member is built both ways instead and a request picks one.
188 + ///
189 + /// Arm 0 is the setting made, matching [`Plan::swap`][swap], which is what
190 + /// the twin reads to decide both which arm it is drawing and whether to
191 + /// make the setting. No body, for [`Arms`](Self::Arms)'s reason: both arms'
192 + /// holes are numbered at the level around them.
193 + ///
194 + /// [swap]: quasi_router::stage::Plan::swap
195 + Swap { guard: Guard },
182 196 /// One dispatch, and which arm the request takes.
183 197 ///
184 198 /// No bodies. Every arm's holes are staged at the level around the
@@ -363,11 +377,8 @@
363 377 "a staged twin cannot be staged again",
364 378 ));
365 379 }
366 - // A guarded member renders where it was written; a guarded SETTING
367 - // renders in its container's opening tag, which is before every member
368 - // regardless of where the declaration put it. The residual marks
369 - // branches in render order and the filler walks them in declaration
370 - // order, so the two would disagree about which branch is which.
380 + // A setting is one of two things, and which one decides whether a
381 + // guard on it can be a mark at all. See [`PLACING`].
371 382 Item::Attribute {
372 383 name,
373 384 args,
@@ -375,17 +386,22 @@
375 386 body,
376 387 } => {
377 388 if let Some(guard) = guard {
378 - return Err(syn::Error::new(
379 - guard.span,
380 - "a staged shape cannot guard a setting yet: a setting varies \
381 - markup INSIDE the member its statement produces, and a mark \
382 - covers a run of members. The answer is two arms over that \
383 - member -- built with the setting and without it -- which is \
384 - the same shape `given` already compiles to",
385 - ));
386 - }
387 - if let Some(guard) = guard {
388 - let staged = self::guard(guard, counters);
389 + if !placing(name) {
390 + return Err(syn::Error::new(
391 + guard.span,
392 + "a staged shape cannot guard a settling setting: it varies \
393 + markup INSIDE the member its own statement produces, and \
394 + a mark covers a run of members. The answer is two arms \
395 + over that member -- built with the setting and without it \
396 + -- which is the shape `given` already compiles to",
397 + ));
398 + }
399 + // A placing setting is a member in all but name, so its guard
400 + // is an ordinary mark over the run it places. The setting is
401 + // made unconditionally and the mark says what a request
402 + // decides, exactly as for a guarded member.
403 + let site = counters.guards;
404 + counters.guards += 1;
389 405 counters.enter();
390 406 let args = staged_args(name, args, counters)?;
391 407 // Inside the branch, because a setting that is not made places
@@ -396,20 +412,74 @@
396 412 guard: (*guard).clone(),
397 413 body: recorded,
398 414 });
399 - return Ok(Item::Attribute {
400 - name: name.clone(),
401 - args,
402 - guard: Some(staged),
403 - body: inner,
415 + return Ok(Item::Marked {
416 + site,
417 + varies: crate::ast::Varies::Absent,
418 + body: vec![Item::Attribute {
419 + name: name.clone(),
420 + args,
421 + guard: None,
422 + body: inner,
423 + }],
404 424 });
405 425 }
426 + // A body may settle the argument under a guard, and that guard
427 + // is the settling case: `chosen` is ` selected` inside the
428 + // `<option>` this same statement produces, so what varies is the
429 + // markup of this member rather than a run of members beside it.
430 + // The member becomes two arms of itself.
431 + let settling: Vec<&Guard> = body
432 + .iter()
433 + .filter_map(|item| match item {
434 + Item::Attribute {
435 + guard: Some(guard),
436 + name,
437 + ..
438 + } if !placing(name) => Some(guard),
439 + _ => None,
440 + })
441 + .collect();
442 + if let Some(guard) = settling.first() {
443 + if settling.len() > 1 {
444 + return Err(syn::Error::new(
445 + guard.span,
446 + "two settling settings on one member would be four arms of \
447 + it, and nothing has wanted that: say one of them another \
448 + way, or teach the twin the combinations",
449 + ));
450 + }
451 + let site = counters.arms;
452 + counters.arms += 1;
453 +
454 + // No scope of its own, for `Fill::Arms`'s reason: both arms'
455 + // holes are numbered at the level around them, so the closure
456 + // filling the stretch these arms sit in is the one that fills
457 + // the arm. That is what lets one arm carry a hole the other
458 + // does not.
459 + let args = staged_args(name, args, counters)?;
460 + let inner = swapped(body, site, counters)?;
461 + counters.wrote(Fill::Swap {
462 + guard: (*guard).clone(),
463 + });
464 +
465 + return Ok(Item::Marked {
466 + site,
467 + varies: crate::ast::Varies::Arm {
468 + of: 2,
469 + taken: plan_arm(site, 2),
470 + },
471 + body: vec![Item::Attribute {
472 + name: name.clone(),
473 + args,
474 + guard: None,
475 + body: inner,
476 + }],
477 + });
478 + }
479 +
406 480 // The argument's own holes render where the argument does, and
407 481 // the body's settings render inside the markup the argument
408 - // builds -- `chosen` is ` selected` in the option's own tag. So
409 - // the args are staged first and the body's guards become branches
410 - // after them. That the branch sits BETWEEN the argument's two
411 - // holes in render order costs nothing: a hole is answered by its
412 - // number, and only branches and loops are walked in order.
482 + // builds. So the args are staged first and the body follows.
413 483 Item::Attribute {
414 484 name: name.clone(),
415 485 args: staged_args(name, args, counters)?,
@@ -530,6 +600,36 @@
530 600 })
531 601 }
532 602
603 + /// One member's body, with its settling guard reading the plan's arm.
604 + ///
605 + /// Arm 0 is the setting made, which is [`Plan::swap`][swap]'s own reading and
606 + /// the one `Fill::Swap` fills against. Everything else in the body is staged
607 + /// the ordinary way.
608 + ///
609 + /// [swap]: quasi_router::stage::Plan::swap
610 + fn swapped(body: &[Item], site: u16, counters: &mut Counters) -> Result<Vec<Item>> {
611 + body.iter()
612 + .map(|item| match item {
613 + Item::Attribute {
614 + name,
615 + args,
616 + guard: Some(_),
617 + body,
618 + } if !placing(name) => Ok(Item::Attribute {
619 + name: name.clone(),
620 + args: staged_args(name, args, counters)?,
621 + guard: Some(Guard {
622 + negated: false,
623 + predicate: Predicate::Truth(plan_read("swap", site)),
624 + span: Span::call_site(),
625 + }),
626 + body: items(body, counters)?,
627 + }),
628 + other => self::item(other, counters),
629 + })
630 + .collect()
631 + }
632 +
533 633 /// A guard becomes one read of the plan, and stops being negated.
534 634 ///
535 635 /// `unless x` and `when not x` are the same question asked twice, and the plan
@@ -642,6 +742,35 @@
642 742 "bar",
643 743 ];
644 744
745 + /// The settings that place a member rather than settling one.
746 + ///
747 + /// A setting is one of two things, and the vocabulary spelled both the same
748 + /// way for a long time.
749 + ///
750 + /// A **placing** setting adds markup at a position of its own. `more` draws a
751 + /// pager after the rows; `figure` accretes onto a strip; `bar` onto a chart;
752 + /// `back`, `forward` and `jumping` each draw their own control inside the
753 + /// pager. Each is a member in all but name, each counts in its container's
754 + /// index space, and a guard on one is an ordinary mark over the run it places.
755 + ///
756 + /// A **settling** setting changes the markup the member its own statement
757 + /// produces was going to write anyway: `chosen` is ` selected` inside an
758 + /// `<option>`, `here` and `latched` are classes inside a tag. There is no run
759 + /// of members to cover, so a guard on one is two arms over that member instead
760 + /// -- quasicoherent `62739b91`.
761 + ///
762 + /// Everything not here is settling, which is the safe default: a setting
763 + /// wrongly called placing marks members its container never drew, and the
764 + /// renderer refuses that by name.
765 + const PLACING: &[&str] = &[
766 + "figure", "bar", "more", "back", "forward", "jumping", "option",
767 + ];
768 +
769 + /// Whether a guard on this setting is a mark rather than two arms.
770 + fn placing(name: &Ident) -> bool {
771 + PLACING.contains(&name.to_string().as_str())
772 + }
773 +
645 774 /// One setting's arguments, staged the way its slot asks for.
646 775 ///
647 776 /// The three answers a slot can give, in the order they are asked: a structure
@@ -1497,6 +1497,26 @@
1497 1497 /// [`paging`](Self::paging) says it, and a renderer marks the jump whose
1498 1498 /// [`Jump::page`] matches.
1499 1499 pub jumps: Vec<Jump>,
1500 + /// What a request decides about runs of the controls this pager draws.
1501 + ///
1502 + /// Empty answering a request, filled by a staged twin. The index space is
1503 + /// back, then the jumps, then forward, which is the order a pager is drawn
1504 + /// in and not the order a declaration may write them in -- see
1505 + /// [`Marking::placed`](crate::stage::Marking::placed), which says that the
1506 + /// numbering is the render order for every container that draws from more
1507 + /// than one list.
1508 + pub marks: crate::stage::Marks,
1509 + }
1510 +
1511 + impl crate::stage::Marking for Rest {
1512 + /// Back, then the jumps, then forward: what a renderer draws, in order.
1513 + fn placed(&self) -> usize {
1514 + usize::from(self.back.is_some()) + self.jumps.len() + usize::from(self.forward.is_some())
1515 + }
1516 +
1517 + fn mark(&mut self, mark: crate::stage::Mark) {
1518 + self.marks.add(mark);
1519 + }
1500 1520 }
1501 1521
1502 1522 /// One page a reader can go straight to.
@@ -1560,6 +1580,7 @@
1560 1580 forward: Some(action),
1561 1581 back: None,
1562 1582 jumps: Vec::new(),
1583 + marks: crate::stage::Marks::none(),
1563 1584 }
1564 1585 }
1565 1586
@@ -1583,6 +1604,7 @@
1583 1604 forward: None,
1584 1605 back: None,
1585 1606 jumps: Vec::new(),
1607 + marks: crate::stage::Marks::none(),
1586 1608 }
1587 1609 }
1588 1610
@@ -1599,6 +1621,7 @@
1599 1621 forward: None,
1600 1622 back: None,
1601 1623 jumps: Vec::new(),
1624 + marks: crate::stage::Marks::none(),
1602 1625 }
1603 1626 }
1604 1627
@@ -580,6 +580,20 @@
580 580 std::iter::repeat_n((), count)
581 581 }
582 582
583 + /// Whether a two-armed site takes its first arm: `arm(id, 2) == 0`.
584 + ///
585 + /// What a guarded SETTLING setting reads. A setting that changes the markup
586 + /// its own member writes -- ` selected` inside an `<option>`, a class inside
587 + /// a tag -- is not a run of members, so it is compiled as two arms over the
588 + /// member instead: one built with the setting, one without. The twin needs
589 + /// the same answer as a bool (to decide whether to make the setting) and as
590 + /// an index (to say which arm it is drawing), and both come from here so
591 + /// the two cannot disagree.
592 + #[must_use]
593 + pub fn swap(&self, id: u16) -> bool {
594 + self.arm(id, 2) == 0
595 + }
596 +
583 597 /// Which arm one dispatch takes. Read by generated code.
584 598 ///
585 599 /// `count` is how many arms the declaration wrote, passed in so a traced
@@ -480,9 +480,24 @@
480 480 out.push('>');
481 481 }
482 482
483 - field.with_layout(|borrowed| {
484 - field_html_into(&borrowed, &filling, opts, out);
485 - });
483 + // A field's members are its options: `option` places one, and `chosen`
484 + // settles the one it places. Told where each landed rather than looking for
485 + // it, since two options with the same label are the same bytes.
486 + let mut marked = crate::stage::Cursor::open(&field.marks);
487 + if marked.watching() {
488 + let mut placed = Vec::new();
489 + field.with_layout(|borrowed| {
490 + makeover_webview::form::field_html_placed(&borrowed, &filling, opts, out, &mut placed);
491 + });
492 + for block in &placed {
493 + marked.wrote(block.start, block.end);
494 + }
495 + marked.close(&field.marks);
496 + } else {
497 + field.with_layout(|borrowed| {
498 + field_html_into(&borrowed, &filling, opts, out);
499 + });
500 + }
486 501
487 502 if writes.is_some() || asked {
488 503 out.push_str("</div>");
@@ -3387,11 +3402,13 @@
3387 3402 // are this crate's: a figure that answers a click becomes a control
3388 3403 // wrapped round the emitted markup rather than a second figure emitter
3389 3404 // that knows about routes.
3390 - Node::Stats { figures, .. } => {
3405 + Node::Stats { figures, marks } => {
3391 3406 out.push_str("<div");
3392 3407 class_attr(&["figures"], opts, out);
3393 3408 out.push('>');
3409 + let mut marked = crate::stage::Cursor::open(marks);
3394 3410 for (figure, action) in figures {
3411 + marked.starts(out);
3395 3412 match action {
3396 3413 Some(action) => {
3397 3414 let (open, close) = control_tag(action);
@@ -3404,7 +3421,9 @@
3404 3421 }
3405 3422 None => figure_html_into(&figure.as_layout(), opts, out),
3406 3423 }
3424 + marked.ends(out);
3407 3425 }
3426 + marked.close(marks);
3408 3427 out.push_str("</div>");
3409 3428 }
3410 3429
@@ -3988,13 +4007,22 @@
3988 4007 class_attr(&["rest"], opts, out);
3989 4008 out.push('>');
3990 4009
4010 + // Back, then the jumps, then forward, matching `Rest::placed`. A pager is
4011 + // the container whose declaration order and render order genuinely differ:
4012 + // the renderer puts Prev first whatever order the declaration said its
4013 + // directions in, which is what `emit::pager_order` makes true rather than
4014 + // hoped for.
4015 + let mut marked = crate::stage::Cursor::open(&rest.marks);
4016 +
3991 4017 if let Some(action) = &rest.back {
4018 + marked.starts(out);
3992 4019 let (open, close) = control_tag(action);
3993 4020 out.push_str(open);
3994 4021 class_attr(&["button", "rest-previous"], opts, out);
3995 4022 action_attrs(action, Fires::Click, None, None, false, out);
3996 4023 out.push_str(">Prev");
3997 4024 out.push_str(close);
4025 + marked.ends(out);
3998 4026 }
3999 4027
4000 4028 if rest.jumps.is_empty() {
@@ -4015,7 +4043,7 @@
4015 4043 }
4016 4044 out.push_str("</span>");
4017 4045 } else {
4018 - rest_strip_html(rest, opts, out);
4046 + rest_strip_html(rest, &mut marked, opts, out);
4019 4047 }
4020 4048
4021 4049 // Written out rather than shared with the Prev branch above. The class name
@@ -4024,14 +4052,17 @@
4024 4052 // scan is how the SSH-keys drift came back. The duplication is the price of
4025 4053 // the check working.
4026 4054 if let Some(action) = &rest.forward {
4055 + marked.starts(out);
4027 4056 let (open, close) = control_tag(action);
4028 4057 out.push_str(open);
4029 4058 class_attr(&["button", "rest-next"], opts, out);
4030 4059 action_attrs(action, Fires::Click, None, None, false, out);
4031 4060 out.push_str(">Next");
4032 4061 out.push_str(close);
4062 + marked.ends(out);
4033 4063 }
4034 4064
4065 + marked.close(&rest.marks);
4035 4066 out.push_str("</div>");
4036 4067 }
4037 4068
@@ -4052,17 +4083,19 @@
4052 4083 /// a control and marks none, which is a host that windowed its pages wrong. It
4053 4084 /// is worth being able to see, and it is not worth refusing to draw a list
4054 4085 /// over.
4055 - fn rest_strip_html(rest: &Rest, opts: &Emit, out: &mut String) {
4086 + fn rest_strip_html(rest: &Rest, marked: &mut crate::stage::Cursor, opts: &Emit, out: &mut String) {
4056 4087 out.push_str("<div");
4057 4088 class_attr(&["rest-pages"], opts, out);
4058 4089 out.push('>');
4059 4090 for jump in &rest.jumps {
4091 + marked.starts(out);
4060 4092 if jump.here {
4061 4093 out.push_str("<span");
4062 4094 class_attr(&["rest-page", "rest-page-here"], opts, out);
4063 4095 out.push_str(" aria-current=\"page\">");
4064 4096 let _ = write!(out, "{}", jump.page);
4065 4097 out.push_str("</span>");
4098 + marked.ends(out);
4066 4099 continue;
4067 4100 }
4068 4101 let (open, close) = control_tag(&jump.action);
@@ -4072,6 +4105,7 @@
4072 4105 out.push('>');
4073 4106 let _ = write!(out, "{}", jump.page);
4074 4107 out.push_str(close);
4108 + marked.ends(out);
4075 4109 }
4076 4110 out.push_str("</div>");
4077 4111 }