Skip to main content

max / quasi

Mark a placeholder's way out, which is the member `offering` places MNW's forum panes are the site: `empty "..." { offering "Browse" to ... unless base.is_empty(); }`. A way out is the one member a placeholder holds, so a guard on it is a run of one -- and `Node::StandIn` counted no members at all, which made the mark loud rather than wrong. That is the refusal working. The renderer hands the placeholder to makeover-webview, so makeover says where the way out went (0.76.0's placeholder_html_placed), wrapper included: the `placeholder-action` element is emitted only when there is something to put in it, so it is part of what disappears.
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 21:20 UTC
Signed with PGP, not checked
Commit: 7e152a4ba3874cb9c217214e641e7662e5895554
Parent: b42a77a
8 files changed, +49 insertions, -17 deletions
M Cargo.lock +6 -6
@@ -2705,9 +2705,9 @@
2705 2705
2706 2706 [[package]]
2707 2707 name = "makeover-webview"
2708 - version = "0.75.1"
2708 + version = "0.76.0"
2709 2709 source = "registry+https://github.com/rust-lang/crates.io-index"
2710 - checksum = "ba1c7d0eaf7a3b6262afb1e3b0e1ad2b1e1aec09182342869a4a0c3e122988dd"
2710 + checksum = "48d1b43ae36a7bb7377fc3d01c6c8485ff9c285a7bf2d332c7bb97145e73de9c"
2711 2711 dependencies = [
2712 2712 "makeover-geometry",
2713 2713 "makeover-layout",
@@ -6314,10 +6314,6 @@
6314 6314 "winnow 1.0.4",
6315 6315 ]
6316 6316
6317 - [[patch.unused]]
6318 - name = "quasi-type"
6319 - version = "0.1.3"
6320 -
6321 6317 [[patch.unused]]
6322 6318 name = "kberg"
6323 6319 version = "0.1.0"
@@ -6337,3 +6333,7 @@
6337 6333 [[patch.unused]]
6338 6334 name = "synckit-client"
6339 6335 version = "0.10.0"
6336 +
6337 + [[patch.unused]]
6338 + name = "quasi-type"
6339 + version = "0.1.3"
@@ -41,7 +41,7 @@
41 41 makeover-layout = "0.44"
42 42 # The renderer's own escaper, so the staged path fills a hole exactly the way
43 43 # `Node::Text` does and the guarantee is not quietly reimplemented.
44 - makeover-webview = "0.75.1"
44 + makeover-webview = "0.76"
45 45 makeover-tui = { version = "0.44", features = ["theme"] }
46 46 # Only to load a bundled theme file. `makeover_tui::Theme` is `#[non_exhaustive]`,
47 47 # so `Theme::from_theme` is the one way to get one, the same reason quasi-tui's
@@ -16,7 +16,7 @@
16 16 quasi-router = { path = "../quasi-router", version = "0.109" }
17 17 quasi-http = { path = "../quasi-http", version = "0.109" }
18 18 makeover-layout = "0.44"
19 - makeover-webview = "0.75.1"
19 + makeover-webview = "0.76"
20 20 # `Node::Rich` carries markdown source and this is what turns it into markup.
21 21 # Sanitising comes with it, which is why the node can carry what a user typed.
22 22 #
@@ -342,6 +342,7 @@
342 342 act: None,
343 343 },
344 344 Node::StandIn {
345 + marks: crate::stage::Marks::none(),
345 346 state: layout::Readiness::Empty,
346 347 message: "nothing yet".into(),
347 348 act: None,
@@ -5638,6 +5638,9 @@
5638 5638 Self::Stats { figures, .. } => figures.len(),
5639 5639 Self::Region(slot) => slot.body.len(),
5640 5640 Self::Canvas(canvas) => canvas.within.len(),
5641 + // One member, and it is one: `offering` places the way out and a
5642 + // guard on it is a run of one.
5643 + Self::StandIn { act, .. } => usize::from(act.is_some()),
5641 5644 _ => 0,
5642 5645 }
5643 5646 }
@@ -5648,7 +5651,8 @@
5648 5651 | Self::Table { marks, .. }
5649 5652 | Self::Timeline { marks, .. }
5650 5653 | Self::Chart { marks, .. }
5651 - | Self::Stats { marks, .. } => marks,
5654 + | Self::Stats { marks, .. }
5655 + | Self::StandIn { marks, .. } => marks,
5652 5656 Self::Region(slot) => &mut slot.marks,
5653 5657 // A node that holds no list has nowhere to keep this, and a twin
5654 5658 // that marked one has marked something it did not build.
@@ -8693,6 +8697,11 @@
8693 8697 /// 2 of goingson's 27 have one and 25 say a sentence and stop, which is
8694 8698 /// why it is optional rather than a second required string.
8695 8699 act: Option<Act>,
8700 + /// What a request decides about the way out.
8701 + ///
8702 + /// The one member a placeholder holds, and it is a member: `offering`
8703 + /// places it, and a guard on it is a run of one. See [`Slot::marks`].
8704 + marks: crate::stage::Marks,
8696 8705 },
8697 8706 /// One control, standing on its own.
8698 8707 ///
@@ -9441,6 +9450,7 @@
9441 9450 /// Nothing here yet.
9442 9451 pub fn empty(message: impl Into<String>) -> Self {
9443 9452 Self::StandIn {
9453 + marks: crate::stage::Marks::none(),
9444 9454 state: layout::Readiness::Empty,
9445 9455 message: message.into(),
9446 9456 act: None,
@@ -9450,6 +9460,7 @@
9450 9460 /// This did not load.
9451 9461 pub fn failed(message: impl Into<String>) -> Self {
9452 9462 Self::StandIn {
9463 + marks: crate::stage::Marks::none(),
9453 9464 state: layout::Readiness::Failed,
9454 9465 message: message.into(),
9455 9466 act: None,
@@ -9471,6 +9482,7 @@
9471 9482 /// every deferred load on every screen.
9472 9483 pub fn pending(message: impl Into<String>) -> Self {
9473 9484 Self::StandIn {
9485 + marks: crate::stage::Marks::none(),
9474 9486 state: layout::Readiness::Pending,
9475 9487 message: message.into(),
9476 9488 act: None,
@@ -563,7 +563,8 @@
563 563 // one holding an `if let`, so that the empty case is named here and not
564 564 // swept into the catch-all below with the members this renderer has
565 565 // never heard of.
566 - Node::StandIn { act: Some(act), .. } | Node::Notice { act: Some(act), .. } => {
566 + Node::StandIn {
567 + act: Some(act), .. } | Node::Notice { act: Some(act), .. } => {
567 568 push_act(act, region, local, found);
568 569 }
569 570 Node::StandIn { act: None, .. } => {}
@@ -405,6 +405,7 @@
405 405 state,
406 406 message,
407 407 act,
408 + ..
408 409 } => {
409 410 let style = match state {
410 411 layout::Readiness::Failed => tui.style().tone(layout::Tone::Danger),
@@ -2897,6 +2897,7 @@
2897 2897 state,
2898 2898 message,
2899 2899 act,
2900 + marks,
2900 2901 } => {
2901 2902 // The markup is makeover-webview's, unchanged, for the reason every
2902 2903 // other emitter here defers to it: the CSS that has to match it is
@@ -2906,13 +2907,29 @@
2906 2907 if let Some(act) = act {
2907 2908 act_html(act, opts, doc, &mut way_out);
2908 2909 }
2909 - placeholder_html_into(
2910 - *state,
2911 - message,
2912 - (!way_out.is_empty()).then_some(Markup(way_out.as_str())),
2913 - opts,
2914 - out,
2915 - );
2910 + let action = (!way_out.is_empty()).then_some(Markup(way_out.as_str()));
2911 + // The way out is the one member a placeholder holds, and the branch
2912 + // covers the wrapper with it: `placeholder-action` is emitted only
2913 + // when there is something to put in it, so it goes when the way out
2914 + // goes.
2915 + let mut marked = crate::stage::Cursor::open(marks);
2916 + if marked.watching() {
2917 + let mut placed = None;
2918 + makeover_webview::placeholder::placeholder_html_placed(
2919 + *state,
2920 + message,
2921 + action,
2922 + opts,
2923 + out,
2924 + &mut placed,
2925 + );
2926 + if let Some(block) = placed {
2927 + marked.wrote(block.start, block.end);
2928 + }
2929 + marked.close(marks);
2930 + } else {
2931 + placeholder_html_into(*state, message, action, opts, out);
2932 + }
2916 2933 }
2917 2934
2918 2935 Node::Rich {