max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
11 files changed,
+735 insertions,
-259 deletions
| @@ -2656,9 +2656,9 @@ | |||
| 2656 | 2656 | ||
| 2657 | 2657 | [[package]] | |
| 2658 | 2658 | name = "makeover-immediate" | |
| 2659 | - | version = "0.45.0" | |
| 2659 | + | version = "0.46.0" | |
| 2660 | 2660 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2661 | - | checksum = "e50588ef96178a8873bc43c370fac6608f554b144b311769c8ca93bb237e3448" | |
| 2661 | + | checksum = "a9868c94a16b8b32c2a30d1e89cc0715fc0044a9663ccb6dffd2dee2464f39fc" | |
| 2662 | 2662 | dependencies = [ | |
| 2663 | 2663 | "egui", | |
| 2664 | 2664 | "egui_extras", | |
| @@ -2668,9 +2668,9 @@ | |||
| 2668 | 2668 | ||
| 2669 | 2669 | [[package]] | |
| 2670 | 2670 | name = "makeover-layout" | |
| 2671 | - | version = "0.44.0" | |
| 2671 | + | version = "0.44.2" | |
| 2672 | 2672 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2673 | - | checksum = "f4c5dd2718ddfe428033a2c55d810e3b567edf9273f148875c1aa0291735ae12" | |
| 2673 | + | checksum = "49914ec86a7383c3f868c22befdca45cae1e72fdfdfb97f988e4075419e2800d" | |
| 2674 | 2674 | ||
| 2675 | 2675 | [[package]] | |
| 2676 | 2676 | name = "makeover-timing" | |
| @@ -2693,9 +2693,9 @@ | |||
| 2693 | 2693 | ||
| 2694 | 2694 | [[package]] | |
| 2695 | 2695 | name = "makeover-tui" | |
| 2696 | - | version = "0.44.0" | |
| 2696 | + | version = "0.44.1" | |
| 2697 | 2697 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2698 | - | checksum = "a39ca8938ca1e082ce16ac772592bc81aa0a14c461c5ec0b60bf55b0728ae7e5" | |
| 2698 | + | checksum = "e0bedfcbe4dcd0635d4802d6e8ab65f5d847621a424a28ec8b1b0aa52a0efa1f" | |
| 2699 | 2699 | dependencies = [ | |
| 2700 | 2700 | "makeover", | |
| 2701 | 2701 | "makeover-layout", | |
| @@ -2705,9 +2705,7 @@ | |||
| 2705 | 2705 | ||
| 2706 | 2706 | [[package]] | |
| 2707 | 2707 | name = "makeover-webview" | |
| 2708 | - | version = "0.74.0" | |
| 2709 | - | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 2710 | - | checksum = "19502720cc808bdf7eead1f2393f270c4418cb1cc5cf0060cc33b31df87af063" | |
| 2708 | + | version = "0.75.0" | |
| 2711 | 2709 | dependencies = [ | |
| 2712 | 2710 | "makeover-geometry", | |
| 2713 | 2711 | "makeover-layout", | |
| @@ -3476,11 +3474,11 @@ | |||
| 3476 | 3474 | ||
| 3477 | 3475 | [[package]] | |
| 3478 | 3476 | name = "quasi" | |
| 3479 | - | version = "0.104.1" | |
| 3477 | + | version = "0.109.0" | |
| 3480 | 3478 | ||
| 3481 | 3479 | [[package]] | |
| 3482 | 3480 | name = "quasi-axum" | |
| 3483 | - | version = "0.104.1" | |
| 3481 | + | version = "0.109.0" | |
| 3484 | 3482 | dependencies = [ | |
| 3485 | 3483 | "axum", | |
| 3486 | 3484 | "http", | |
| @@ -3494,7 +3492,7 @@ | |||
| 3494 | 3492 | ||
| 3495 | 3493 | [[package]] | |
| 3496 | 3494 | name = "quasi-basics" | |
| 3497 | - | version = "0.104.1" | |
| 3495 | + | version = "0.109.0" | |
| 3498 | 3496 | dependencies = [ | |
| 3499 | 3497 | "makeover-layout", | |
| 3500 | 3498 | "quasi-declare", | |
| @@ -3505,7 +3503,7 @@ | |||
| 3505 | 3503 | ||
| 3506 | 3504 | [[package]] | |
| 3507 | 3505 | name = "quasi-bench" | |
| 3508 | - | version = "0.104.1" | |
| 3506 | + | version = "0.109.0" | |
| 3509 | 3507 | dependencies = [ | |
| 3510 | 3508 | "askama", | |
| 3511 | 3509 | "dhat", | |
| @@ -3526,7 +3524,7 @@ | |||
| 3526 | 3524 | ||
| 3527 | 3525 | [[package]] | |
| 3528 | 3526 | name = "quasi-declare" | |
| 3529 | - | version = "0.1.13" | |
| 3527 | + | version = "0.1.15" | |
| 3530 | 3528 | dependencies = [ | |
| 3531 | 3529 | "proc-macro2", | |
| 3532 | 3530 | "quasi-router", | |
| @@ -3538,7 +3536,7 @@ | |||
| 3538 | 3536 | ||
| 3539 | 3537 | [[package]] | |
| 3540 | 3538 | name = "quasi-http" | |
| 3541 | - | version = "0.104.1" | |
| 3539 | + | version = "0.109.0" | |
| 3542 | 3540 | dependencies = [ | |
| 3543 | 3541 | "form_urlencoded", | |
| 3544 | 3542 | "http", | |
| @@ -3547,7 +3545,7 @@ | |||
| 3547 | 3545 | ||
| 3548 | 3546 | [[package]] | |
| 3549 | 3547 | name = "quasi-immediate" | |
| 3550 | - | version = "0.104.1" | |
| 3548 | + | version = "0.109.0" | |
| 3551 | 3549 | dependencies = [ | |
| 3552 | 3550 | "docengine", | |
| 3553 | 3551 | "egui", | |
| @@ -3558,7 +3556,7 @@ | |||
| 3558 | 3556 | ||
| 3559 | 3557 | [[package]] | |
| 3560 | 3558 | name = "quasi-notifs" | |
| 3561 | - | version = "0.104.1" | |
| 3559 | + | version = "0.109.0" | |
| 3562 | 3560 | dependencies = [ | |
| 3563 | 3561 | "quasi-router", | |
| 3564 | 3562 | "synckit-config", | |
| @@ -3568,7 +3566,7 @@ | |||
| 3568 | 3566 | ||
| 3569 | 3567 | [[package]] | |
| 3570 | 3568 | name = "quasi-router" | |
| 3571 | - | version = "0.104.1" | |
| 3569 | + | version = "0.109.0" | |
| 3572 | 3570 | dependencies = [ | |
| 3573 | 3571 | "makeover-layout", | |
| 3574 | 3572 | ] | |
| @@ -3585,7 +3583,7 @@ | |||
| 3585 | 3583 | ||
| 3586 | 3584 | [[package]] | |
| 3587 | 3585 | name = "quasi-tauri" | |
| 3588 | - | version = "0.104.1" | |
| 3586 | + | version = "0.109.0" | |
| 3589 | 3587 | dependencies = [ | |
| 3590 | 3588 | "http", | |
| 3591 | 3589 | "quasi-http", | |
| @@ -3596,7 +3594,7 @@ | |||
| 3596 | 3594 | ||
| 3597 | 3595 | [[package]] | |
| 3598 | 3596 | name = "quasi-tui" | |
| 3599 | - | version = "0.104.1" | |
| 3597 | + | version = "0.109.0" | |
| 3600 | 3598 | dependencies = [ | |
| 3601 | 3599 | "docengine", | |
| 3602 | 3600 | "makeover", | |
| @@ -3609,7 +3607,7 @@ | |||
| 3609 | 3607 | ||
| 3610 | 3608 | [[package]] | |
| 3611 | 3609 | name = "quasi-webview" | |
| 3612 | - | version = "0.104.1" | |
| 3610 | + | version = "0.109.0" | |
| 3613 | 3611 | dependencies = [ | |
| 3614 | 3612 | "docengine", | |
| 3615 | 3613 | "makeover-layout", | |
| @@ -6314,14 +6312,14 @@ | |||
| 6314 | 6312 | "winnow 1.0.4", | |
| 6315 | 6313 | ] | |
| 6316 | 6314 | ||
| 6317 | - | [[patch.unused]] | |
| 6318 | - | name = "synckit-client" | |
| 6319 | - | version = "0.10.0" | |
| 6320 | - | ||
| 6321 | 6315 | [[patch.unused]] | |
| 6322 | 6316 | name = "quasi-type" | |
| 6323 | 6317 | version = "0.1.3" | |
| 6324 | 6318 | ||
| 6319 | + | [[patch.unused]] | |
| 6320 | + | name = "synckit-client" | |
| 6321 | + | version = "0.10.0" | |
| 6322 | + | ||
| 6325 | 6323 | [[patch.unused]] | |
| 6326 | 6324 | name = "kberg" | |
| 6327 | 6325 | version = "0.1.0" |
| @@ -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.74" | |
| 44 | + | makeover-webview = "0.75" | |
| 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.74" | |
| 19 | + | makeover-webview = "0.75" | |
| 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 | # |
| @@ -86,6 +86,42 @@ | |||
| 86 | 86 | }, | |
| 87 | 87 | /// Anything that puts something into the enclosing container. | |
| 88 | 88 | Emit(Emission), | |
| 89 | + | /// A run of members whose markup a request decides, and which site decides | |
| 90 | + | /// it. | |
| 91 | + | /// | |
| 92 | + | /// Written by no author and parsed from nothing. [`crate::symbolic`] puts | |
| 93 | + | /// these in when it rewrites a declaration into its staged twin: a guard | |
| 94 | + | /// becomes an unguarded body under a mark, a loop becomes one pass under a | |
| 95 | + | /// mark, and a dispatch becomes the arm the plan picked under a mark. | |
| 96 | + | /// | |
| 97 | + | /// # Why the twin marks rather than the renderer inferring | |
| 98 | + | /// | |
| 99 | + | /// The macro parsed the guard, so it knows exactly which members it | |
| 100 | + | /// controls. A derivation that rendered the screen twice and measured the | |
| 101 | + | /// difference would be recovering something that was never lost, and every | |
| 102 | + | /// bug that path had was in the recovery rather than in the description. | |
| 103 | + | Marked { | |
| 104 | + | /// The site, numbered by the same counter the plan uses. | |
| 105 | + | site: u16, | |
| 106 | + | /// What a request decides about the run. | |
| 107 | + | varies: Varies, | |
| 108 | + | body: Vec<Item>, | |
| 109 | + | }, | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | /// What a request decides about a marked run, as the twin writes it. | |
| 113 | + | /// | |
| 114 | + | /// The compile-time twin of [`quasi_router::stage::Varies`], which is the | |
| 115 | + | /// runtime value this becomes. Separate because an arm's index is a hole read | |
| 116 | + | /// from the plan rather than a number the macro knows. | |
| 117 | + | #[derive(Clone)] | |
| 118 | + | pub enum Varies { | |
| 119 | + | /// A guard: the run is placed, or it is not. | |
| 120 | + | Absent, | |
| 121 | + | /// A loop: the run is one pass. | |
| 122 | + | Repeated, | |
| 123 | + | /// A dispatch: `of` arms, and the one the plan asked for. | |
| 124 | + | Arm { of: usize, taken: Hole }, | |
| 89 | 125 | } | |
| 90 | 126 | ||
| 91 | 127 | #[derive(Clone)] |
| @@ -261,6 +261,16 @@ | |||
| 261 | 261 | let value = source_value(source)?; | |
| 262 | 262 | bindings.push(quote!(let #name = #value;)); | |
| 263 | 263 | } | |
| 264 | + | // A value is not a container, so there is no run of members for a | |
| 265 | + | // mark to cover. Reached only if the twin marked a body answering a | |
| 266 | + | // value, which is a bug in the rewrite rather than in a screen. | |
| 267 | + | Item::Marked { .. } => { | |
| 268 | + | return Err(syn::Error::new( | |
| 269 | + | span, | |
| 270 | + | "a marked run in a body that answers a value, which has no \ | |
| 271 | + | members to mark", | |
| 272 | + | )); | |
| 273 | + | } | |
| 264 | 274 | Item::Attribute { name, .. } => { | |
| 265 | 275 | return Err(syn::Error::new( | |
| 266 | 276 | name.span(), | |
| @@ -406,6 +416,7 @@ | |||
| 406 | 416 | matches!( | |
| 407 | 417 | item, | |
| 408 | 418 | Item::For { .. } | |
| 419 | + | | Item::Marked { .. } | |
| 409 | 420 | | Item::Emit(Emission::Guarded { .. }) | |
| 410 | 421 | | Item::Attribute { guard: Some(_), .. } | |
| 411 | 422 | ) | |
| @@ -425,7 +436,9 @@ | |||
| 425 | 436 | Item::Attribute { | |
| 426 | 437 | name, args, body, .. | |
| 427 | 438 | } => steps.push(attribute(name, args, body)?), | |
| 428 | - | Item::For { .. } => unreachable!("a loop takes the accumulating form"), | |
| 439 | + | Item::For { .. } | Item::Marked { .. } => { | |
| 440 | + | unreachable!("a loop and a marked run take the accumulating form") | |
| 441 | + | } | |
| 429 | 442 | Item::Emit(emission) => { | |
| 430 | 443 | // A member is evaluated into its own binding before the | |
| 431 | 444 | // container is built, because a container's own name is often | |
| @@ -500,6 +513,31 @@ | |||
| 500 | 513 | let binder = binder_pattern(*dereferenced, binder); | |
| 501 | 514 | quote!(for #binder in #iterable { #(#inner)* }) | |
| 502 | 515 | } | |
| 516 | + | Item::Marked { site, varies, body } => { | |
| 517 | + | let inner = self::statements(body, container, built)?; | |
| 518 | + | let varies = self::varies(varies)?; | |
| 519 | + | let plan = format_ident!("{}", crate::symbolic::PLAN, span = Span::call_site()); | |
| 520 | + | let at = format_ident!("marked_from_{index}", span = Span::call_site()); | |
| 521 | + | // `placed` before and after, so the run is named by where its | |
| 522 | + | // members landed rather than by counting what was written. A | |
| 523 | + | // container that cannot be marked fails here, naming its own | |
| 524 | + | // type, which is how "can this shape be staged" stays a | |
| 525 | + | // question rustc answers rather than a list somebody keeps. | |
| 526 | + | quote!({ | |
| 527 | + | let #at = ::quasi_router::stage::Marking::placed(&#built); | |
| 528 | + | #(#inner)* | |
| 529 | + | ::quasi_router::stage::Marking::mark( | |
| 530 | + | &mut #built, | |
| 531 | + | ::quasi_router::stage::Mark { | |
| 532 | + | scope: #plan.scope(), | |
| 533 | + | id: #site, | |
| 534 | + | varies: #varies, | |
| 535 | + | from: #at, | |
| 536 | + | to: ::quasi_router::stage::Marking::placed(&#built), | |
| 537 | + | }, | |
| 538 | + | ); | |
| 539 | + | }) | |
| 540 | + | } | |
| 503 | 541 | Item::Emit(Emission::Guarded { guard, inner }) => { | |
| 504 | 542 | let test = predicate(guard)?; | |
| 505 | 543 | let held = format_ident!("member_{index}", span = Span::call_site()); | |
| @@ -525,6 +563,22 @@ | |||
| 525 | 563 | Ok(statements) | |
| 526 | 564 | } | |
| 527 | 565 | ||
| 566 | + | /// What a marked run says at runtime. | |
| 567 | + | /// | |
| 568 | + | /// An arm's index is read from the plan rather than known here, which is the | |
| 569 | + | /// whole reason [`crate::ast::Varies`] is its own type: the other two are | |
| 570 | + | /// decided by the macro, and this one is decided per render. | |
| 571 | + | fn varies(varies: &crate::ast::Varies) -> Result<TokenStream> { | |
| 572 | + | Ok(match varies { | |
| 573 | + | crate::ast::Varies::Absent => quote!(::quasi_router::stage::Varies::Absent), | |
| 574 | + | crate::ast::Varies::Repeated => quote!(::quasi_router::stage::Varies::Repeated), | |
| 575 | + | crate::ast::Varies::Arm { of, taken } => { | |
| 576 | + | let taken = hole(taken)?; | |
| 577 | + | quote!(::quasi_router::stage::Varies::Arm { of: #of, at: #taken }) | |
| 578 | + | } | |
| 579 | + | }) | |
| 580 | + | } | |
| 581 | + | ||
| 528 | 582 | /// One setting, as the builder call ATTRIBUTE NAMING says it is. | |
| 529 | 583 | /// | |
| 530 | 584 | /// A body is settings made on the argument before it is passed. See | |
| @@ -579,6 +633,14 @@ | |||
| 579 | 633 | items | |
| 580 | 634 | .iter() | |
| 581 | 635 | .map(|item| match item { | |
| 636 | + | // Settling an argument is not accreting members either. A guard on | |
| 637 | + | // a setting inside one of these varies markup INSIDE the member its | |
| 638 | + | // statement produces, so the mark belongs to the container that | |
| 639 | + | // member lands in, which is where the twin puts it. | |
| 640 | + | Item::Marked { .. } => Err(syn::Error::new( | |
| 641 | + | proc_macro2::Span::call_site(), | |
| 642 | + | "a marked run where an argument is settled, which has no members", | |
| 643 | + | )), | |
| 582 | 644 | Item::Attribute { | |
| 583 | 645 | name, | |
| 584 | 646 | args, | |
| @@ -657,7 +719,9 @@ | |||
| 657 | 719 | ) -> Result<()> { | |
| 658 | 720 | for item in items { | |
| 659 | 721 | match item { | |
| 660 | - | Item::For { body, .. } => each(body, last, last_name)?, | |
| 722 | + | Item::For { body, .. } | Item::Marked { body, .. } => { | |
| 723 | + | each(body, last, last_name)?; | |
| 724 | + | } | |
| 661 | 725 | Item::Attribute { name, .. } => { | |
| 662 | 726 | let Some(at) = drawn(name) else { continue }; | |
| 663 | 727 | if at < *last { | |
| @@ -1605,6 +1669,18 @@ | |||
| 1605 | 1669 | let held = format_ident!("asked", span = Span::call_site()); | |
| 1606 | 1670 | for (index, item) in body.iter().enumerate() { | |
| 1607 | 1671 | match item { | |
| 1672 | + | // A form's fields are a bare `Vec`, so there is no container | |
| 1673 | + | // carrying marks for them yet. Refused rather than dropped: a | |
| 1674 | + | // staged form with a guarded field would otherwise compile to a | |
| 1675 | + | // residual with no branch where the fill program has one, and the | |
| 1676 | + | // filler would find that out on a request. | |
| 1677 | + | Item::Marked { .. } => { | |
| 1678 | + | return Err(syn::Error::new( | |
| 1679 | + | Span::call_site(), | |
| 1680 | + | "a form's fields cannot be marked yet, so this shape cannot be \ | |
| 1681 | + | staged; give `Node::Form` its own marks first", | |
| 1682 | + | )); | |
| 1683 | + | } | |
| 1608 | 1684 | Item::Attribute { | |
| 1609 | 1685 | name, | |
| 1610 | 1686 | args, | |
| @@ -1759,6 +1835,7 @@ | |||
| 1759 | 1835 | ) { | |
| 1760 | 1836 | for item in items { | |
| 1761 | 1837 | match item { | |
| 1838 | + | Item::Marked { body, .. } => marking(body, value, marked), | |
| 1762 | 1839 | Item::Attribute { name, body, .. } => { | |
| 1763 | 1840 | if name == "value" { | |
| 1764 | 1841 | value.get_or_insert(name); | |
| @@ -1814,6 +1891,15 @@ | |||
| 1814 | 1891 | items | |
| 1815 | 1892 | .iter() | |
| 1816 | 1893 | .map(|item| match item { | |
| 1894 | + | // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the | |
| 1895 | + | // wrapper that fixes it, and until this builder uses one, a shape | |
| 1896 | + | // whose members accrete into a list cannot be staged. Refused | |
| 1897 | + | // rather than dropped: a residual missing a branch the fill program | |
| 1898 | + | // has is a fault a request discovers. | |
| 1899 | + | Item::Marked { .. } => Err(syn::Error::new( | |
| 1900 | + | Span::call_site(), | |
| 1901 | + | "a menu's entries cannot be marked yet, so this shape cannot be staged", | |
| 1902 | + | )), | |
| 1817 | 1903 | Item::Bind { name, source } => { | |
| 1818 | 1904 | let value = source_value(source)?; | |
| 1819 | 1905 | Ok(quote!(let #name = #value;)) | |
| @@ -1858,6 +1944,15 @@ | |||
| 1858 | 1944 | items | |
| 1859 | 1945 | .iter() | |
| 1860 | 1946 | .map(|item| match item { | |
| 1947 | + | // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the | |
| 1948 | + | // wrapper that fixes it, and until this builder uses one, a shape | |
| 1949 | + | // whose members accrete into a list cannot be staged. Refused | |
| 1950 | + | // rather than dropped: a residual missing a branch the fill program | |
| 1951 | + | // has is a fault a request discovers. | |
| 1952 | + | Item::Marked { .. } => Err(syn::Error::new( | |
| 1953 | + | Span::call_site(), | |
| 1954 | + | "a panel's members cannot be marked yet, so this shape cannot be staged", | |
| 1955 | + | )), | |
| 1861 | 1956 | Item::Bind { name, source } => { | |
| 1862 | 1957 | let value = source_value(source)?; | |
| 1863 | 1958 | Ok(quote!(let #name = #value;)) | |
| @@ -1998,6 +2093,15 @@ | |||
| 1998 | 2093 | items | |
| 1999 | 2094 | .iter() | |
| 2000 | 2095 | .map(|item| match item { | |
| 2096 | + | // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the | |
| 2097 | + | // wrapper that fixes it, and until this builder uses one, a shape | |
| 2098 | + | // whose members accrete into a list cannot be staged. Refused | |
| 2099 | + | // rather than dropped: a residual missing a branch the fill program | |
| 2100 | + | // has is a fault a request discovers. | |
| 2101 | + | Item::Marked { .. } => Err(syn::Error::new( | |
| 2102 | + | Span::call_site(), | |
| 2103 | + | "a list's rows cannot be marked yet, so this shape cannot be staged", | |
| 2104 | + | )), | |
| 2001 | 2105 | Item::Bind { name, source } => { | |
| 2002 | 2106 | let value = source_value(source)?; | |
| 2003 | 2107 | Ok(quote!(let #name = #value;)) |
| @@ -174,7 +174,7 @@ | |||
| 174 | 174 | } | |
| 175 | 175 | } | |
| 176 | 176 | Item::Attribute { args, .. } => args.iter().for_each(|arg| walk_arg(arg, found)), | |
| 177 | - | Item::For { body, .. } => walk_items(body, found), | |
| 177 | + | Item::For { body, .. } | Item::Marked { body, .. } => walk_items(body, found), | |
| 178 | 178 | Item::Emit(emission) => walk_emission(emission, found), | |
| 179 | 179 | } | |
| 180 | 180 | } |
| @@ -137,7 +137,7 @@ | |||
| 137 | 137 | } | |
| 138 | 138 | ||
| 139 | 139 | /// The parameter the staged function takes in place of the request's reads. | |
| 140 | - | const PLAN: &str = "plan"; | |
| 140 | + | pub(crate) const PLAN: &str = "plan"; | |
| 141 | 141 | ||
| 142 | 142 | /// The staged twin of `name`, which is what an `include` retargets to. | |
| 143 | 143 | pub fn staged_name(name: &Ident) -> Ident { | |
| @@ -354,6 +354,15 @@ | |||
| 354 | 354 | name: name.clone(), | |
| 355 | 355 | source: self::source(source, counters)?, | |
| 356 | 356 | }, | |
| 357 | + | // Nothing an author wrote. A twin is built from a declaration, and a | |
| 358 | + | // declaration cannot carry one of these, so meeting one means the | |
| 359 | + | // rewrite has been run over its own output. | |
| 360 | + | Item::Marked { .. } => { | |
| 361 | + | return Err(syn::Error::new( | |
| 362 | + | Span::call_site(), | |
| 363 | + | "a staged twin cannot be staged again", | |
| 364 | + | )); | |
| 365 | + | } | |
| 357 | 366 | // A guarded member renders where it was written; a guarded SETTING | |
| 358 | 367 | // renders in its container's opening tag, which is before every member | |
| 359 | 368 | // regardless of where the declaration put it. The residual marks | |
| @@ -429,11 +438,92 @@ | |||
| 429 | 438 | iterable: iterable.clone(), | |
| 430 | 439 | body: inner, | |
| 431 | 440 | }); | |
| 432 | - | Item::For { | |
| 433 | - | dereferenced: false, | |
| 434 | - | binder: format_ident!("_{}", binder, span = binder.span()), | |
| 435 | - | iterable: plan_read("rows", id), | |
| 436 | - | body: staged, | |
| 441 | + | Item::Marked { | |
| 442 | + | site: id, | |
| 443 | + | varies: crate::ast::Varies::Repeated, | |
| 444 | + | body: vec![Item::For { | |
| 445 | + | dereferenced: false, | |
| 446 | + | binder: format_ident!("_{}", binder, span = binder.span()), | |
| 447 | + | iterable: plan_read("rows", id), | |
| 448 | + | body: staged, | |
| 449 | + | }], | |
| 450 | + | } | |
| 451 | + | } | |
| 452 | + | // A guarded member is placed unconditionally and marked instead. | |
| 453 | + | // | |
| 454 | + | // The guard is what named the members it controls, so the twin says so | |
| 455 | + | // and the renderer records where they landed. Rendering the screen a | |
| 456 | + | // second time with the guard off, to see what went missing, is the | |
| 457 | + | // thing this replaces. | |
| 458 | + | Item::Emit(Emission::Guarded { guard, inner }) => { | |
| 459 | + | let site = counters.guards; | |
| 460 | + | counters.guards += 1; | |
| 461 | + | counters.enter(); | |
| 462 | + | let inner = self::emission(inner, counters)?; | |
| 463 | + | let body = counters.exit(); | |
| 464 | + | counters.wrote(Fill::Branch { | |
| 465 | + | guard: (*guard).clone(), | |
| 466 | + | body, | |
| 467 | + | }); | |
| 468 | + | Item::Marked { | |
| 469 | + | site, | |
| 470 | + | varies: crate::ast::Varies::Absent, | |
| 471 | + | body: vec![Item::Emit(inner)], | |
| 472 | + | } | |
| 473 | + | } | |
| 474 | + | // A dispatch draws the arm the plan asks for, and says how many there | |
| 475 | + | // are. The derivation renders it once per arm and reads each arm's own | |
| 476 | + | // cover, so an arm may hold loops and guards of its own -- which is | |
| 477 | + | // what "the arms are flat" used to be about. | |
| 478 | + | Item::Emit(Emission::Given { | |
| 479 | + | scrutinee, | |
| 480 | + | arms, | |
| 481 | + | otherwise, | |
| 482 | + | }) => { | |
| 483 | + | let Some(otherwise) = otherwise else { | |
| 484 | + | return Err(syn::Error::new( | |
| 485 | + | scrutinee.span(), | |
| 486 | + | "a staged dispatch says what it does when nothing matches: \ | |
| 487 | + | a residual holds one arm per position and there has to be \ | |
| 488 | + | one to hold", | |
| 489 | + | )); | |
| 490 | + | }; | |
| 491 | + | let site = counters.arms; | |
| 492 | + | counters.arms += 1; | |
| 493 | + | ||
| 494 | + | // Every arm's holes are numbered at THIS level rather than inside | |
| 495 | + | // the arm, which is what lets one arm carry a hole another does not. | |
| 496 | + | // See `Fill::Arms`. | |
| 497 | + | let staged = arms | |
| 498 | + | .iter() | |
| 499 | + | .map(|(pattern, arm)| { | |
| 500 | + | Ok((pattern.clone(), Box::new(self::emission(arm, counters)?))) | |
| 501 | + | }) | |
| 502 | + | .collect::<Result<Vec<_>>>()?; | |
| 503 | + | let last = Box::new(self::emission(otherwise, counters)?); | |
| 504 | + | ||
| 505 | + | counters.wrote(Fill::Arms { | |
| 506 | + | scrutinee: (*scrutinee).clone(), | |
| 507 | + | patterns: arms.iter().map(|(pattern, _)| pattern.clone()).collect(), | |
| 508 | + | }); | |
| 509 | + | ||
| 510 | + | let of = staged.len() + 1; | |
| 511 | + | let taken = plan_arm(site, of); | |
| 512 | + | Item::Marked { | |
| 513 | + | site, | |
| 514 | + | varies: crate::ast::Varies::Arm { | |
| 515 | + | of, | |
| 516 | + | taken: taken.clone(), | |
| 517 | + | }, | |
| 518 | + | body: vec![Item::Emit(Emission::Given { | |
| 519 | + | scrutinee: taken, | |
| 520 | + | arms: staged | |
| 521 | + | .into_iter() | |
| 522 | + | .enumerate() | |
| 523 | + | .map(|(at, (_, arm))| (crate::ast::Pattern::Int(at as i64), arm)) | |
| 524 | + | .collect(), | |
| 525 | + | otherwise: Some(last), | |
| 526 | + | })], | |
| 437 | 527 | } | |
| 438 | 528 | } | |
| 439 | 529 | Item::Emit(emission) => Item::Emit(self::emission(emission, counters)?), |
| @@ -5223,6 +5223,13 @@ | |||
| 5223 | 5223 | /// a renderer narrows a region the way it already narrows a table: raise a | |
| 5224 | 5224 | /// cutoff over a declared order. `Ranked`'s own docs carry the argument. | |
| 5225 | 5225 | pub body: Body, | |
| 5226 | + | /// What a request decides about runs of [`body`](Self::body). | |
| 5227 | + | /// | |
| 5228 | + | /// Empty on every tree built to answer a request. A staged twin fills it, | |
| 5229 | + | /// and it is what lets a renderer bracket the markup a guard, a loop or a | |
| 5230 | + | /// dispatch controls rather than leaving a derivation to find those bounds | |
| 5231 | + | /// by comparing renders. See [`stage::Mark`](crate::stage::Mark). | |
| 5232 | + | pub marks: crate::stage::Marks, | |
| 5226 | 5233 | /// How many of [`body`](Self::body) are visible at once. | |
| 5227 | 5234 | /// | |
| 5228 | 5235 | /// [`layout::Showing::All`] by default, so a description written against | |
| @@ -5561,6 +5568,32 @@ | |||
| 5561 | 5568 | } | |
| 5562 | 5569 | } | |
| 5563 | 5570 | ||
| 5571 | + | impl crate::stage::Marking for Slot { | |
| 5572 | + | /// Members of the body, whichever shape it has. | |
| 5573 | + | fn placed(&self) -> usize { | |
| 5574 | + | self.body.len() | |
| 5575 | + | } | |
| 5576 | + | ||
| 5577 | + | fn mark(&mut self, mark: crate::stage::Mark) { | |
| 5578 | + | self.marks.add(mark); | |
| 5579 | + | } | |
| 5580 | + | } | |
| 5581 | + | ||
| 5582 | + | impl crate::stage::Marking for Row { | |
| 5583 | + | /// Cells first, then the menu, which is the order a row is drawn in. | |
| 5584 | + | /// | |
| 5585 | + | /// One index space over both lists, which is the rule for every container | |
| 5586 | + | /// that draws from more than one: the numbering is the render order, so a | |
| 5587 | + | /// mark reads the same to the renderer as it did to the twin that made it. | |
| 5588 | + | fn placed(&self) -> usize { | |
| 5589 | + | self.cells.len() + self.menu.len() | |
| 5590 | + | } | |
| 5591 | + | ||
| 5592 | + | fn mark(&mut self, mark: crate::stage::Mark) { | |
| 5593 | + | self.marks.add(mark); | |
| 5594 | + | } | |
| 5595 | + | } | |
| 5596 | + | ||
| 5564 | 5597 | impl Slot { | |
| 5565 | 5598 | /// An empty region under this address. | |
| 5566 | 5599 | pub fn new(id: impl Into<String>, kind: RegionKind) -> Self { | |
| @@ -5569,6 +5602,7 @@ | |||
| 5569 | 5602 | kind, | |
| 5570 | 5603 | readiness: layout::Readiness::Ready, | |
| 5571 | 5604 | body: Body::All(Vec::new()), | |
| 5605 | + | marks: crate::stage::Marks::none(), | |
| 5572 | 5606 | name: None, | |
| 5573 | 5607 | fed_by: None, | |
| 5574 | 5608 | live: false, | |
| @@ -6860,6 +6894,11 @@ | |||
| 6860 | 6894 | /// them. A cell keyed [`CellKey::Named`] is unresolved until | |
| 6861 | 6895 | /// [`Table::row`] sees it. | |
| 6862 | 6896 | pub cells: Vec<Cell>, | |
| 6897 | + | /// What a request decides about runs of [`cells`](Self::cells). | |
| 6898 | + | /// | |
| 6899 | + | /// Empty answering a request, filled by a staged twin. See | |
| 6900 | + | /// [`Slot::marks`], which carries the argument for both. | |
| 6901 | + | pub marks: crate::stage::Marks, | |
| 6863 | 6902 | /// The route that selects this row, if selecting it does anything. | |
| 6864 | 6903 | pub activate: Option<Action>, | |
| 6865 | 6904 | /// Whether this is the row the detail side is currently showing. |
| @@ -192,6 +192,185 @@ | |||
| 192 | 192 | pub arms: BTreeSet<(u32, u16, usize)>, | |
| 193 | 193 | } | |
| 194 | 194 | ||
| 195 | + | /// What a request decides about the members one mark covers. | |
| 196 | + | /// | |
| 197 | + | /// The three ways markup varies, and there is no fourth: a run of members is | |
| 198 | + | /// there or is not, is drawn once per element a request brought, or is one of | |
| 199 | + | /// several drawings of which a request picks one. | |
| 200 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 201 | + | pub enum Varies { | |
| 202 | + | /// A guard: the members are placed, or they are not. | |
| 203 | + | Absent, | |
| 204 | + | /// A loop: the members are one pass, drawn once per element. | |
| 205 | + | Repeated, | |
| 206 | + | /// A dispatch: this is arm `at` of `of`, and a request picks one of them. | |
| 207 | + | Arm { | |
| 208 | + | /// How many arms the dispatch offers. | |
| 209 | + | of: usize, | |
| 210 | + | /// Which one this is. | |
| 211 | + | at: usize, | |
| 212 | + | }, | |
| 213 | + | } | |
| 214 | + | ||
| 215 | + | /// One run of a container's members whose markup a request decides. | |
| 216 | + | /// | |
| 217 | + | /// # Why the container carries this and the member does not | |
| 218 | + | /// | |
| 219 | + | /// What varies is never one value in a node, it is a run of a container's | |
| 220 | + | /// members: a guard on a member covers one, a loop body covers however many | |
| 221 | + | /// the pass emitted, and a dispatch's arm covers whatever that arm wrote. | |
| 222 | + | /// Naming the run by its bounds in the container says all three the same way, | |
| 223 | + | /// and leaves every member type untouched. | |
| 224 | + | /// | |
| 225 | + | /// A renderer reads these only while it is staging. Answering a request, a | |
| 226 | + | /// container's marks are empty and nothing consults them. | |
| 227 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 228 | + | pub struct Mark { | |
| 229 | + | /// Which shape's numbering `id` belongs to. See [`Plan::scope`]. | |
| 230 | + | pub scope: u32, | |
| 231 | + | /// The site, numbered by the macro that parsed it. | |
| 232 | + | pub id: u16, | |
| 233 | + | /// What a request decides about it. | |
| 234 | + | pub varies: Varies, | |
| 235 | + | /// The first member covered. | |
| 236 | + | pub from: usize, | |
| 237 | + | /// One past the last member covered. | |
| 238 | + | pub to: usize, | |
| 239 | + | } | |
| 240 | + | ||
| 241 | + | /// The marks a container carries, which is none while a request is answered. | |
| 242 | + | /// | |
| 243 | + | /// A `None` inside rather than an empty `Vec` because every container in the | |
| 244 | + | /// tree has one of these on the serving path and none of them uses it: the | |
| 245 | + | /// marks exist only in the tree a staged twin builds. This way that costs a | |
| 246 | + | /// null pointer per container rather than an allocation. | |
| 247 | + | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 248 | + | pub struct Marks(Option<Box<Vec<Mark>>>); | |
| 249 | + | ||
| 250 | + | impl Marks { | |
| 251 | + | /// Nothing marked, which is what answering a request builds. | |
| 252 | + | #[must_use] | |
| 253 | + | pub const fn none() -> Self { | |
| 254 | + | Self(None) | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | /// Whether anything here is marked. | |
| 258 | + | #[must_use] | |
| 259 | + | pub fn is_empty(&self) -> bool { | |
| 260 | + | self.0.as_ref().is_none_or(|marks| marks.is_empty()) | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | /// Every mark, outermost first, in the order they were made. | |
| 264 | + | #[must_use] | |
| 265 | + | pub fn all(&self) -> &[Mark] { | |
| 266 | + | self.0.as_ref().map_or(&[], |marks| marks.as_slice()) | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | /// Cover members `from..to` with one mark. | |
| 270 | + | pub fn add(&mut self, mark: Mark) { | |
| 271 | + | self.0.get_or_insert_with(Box::default).push(mark); | |
| 272 | + | } | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | /// A container a staged twin can mark a run of members in. | |
| 276 | + | /// | |
| 277 | + | /// One trait so the macro emits the same three lines whatever it is building: | |
| 278 | + | /// read where the container is up to, emit, then cover what was emitted. A | |
| 279 | + | /// container that has not implemented this fails to compile at the site that | |
| 280 | + | /// tried to mark it, naming the type, which is the answer to "can this shape be | |
| 281 | + | /// staged" being a compiler question rather than a list somebody maintains. | |
| 282 | + | pub trait Marking { | |
| 283 | + | /// How many members have been placed so far. | |
| 284 | + | /// | |
| 285 | + | /// The unit is whatever the renderer walks, so a container whose members | |
| 286 | + | /// are drawn from more than one list counts them the way it draws them. | |
| 287 | + | fn placed(&self) -> usize; | |
| 288 | + | ||
| 289 | + | /// Cover a run of members with one mark. | |
| 290 | + | fn mark(&mut self, mark: Mark); | |
| 291 | + | ||
| 292 | + | /// Take a spliced shape's own marks, shifted to where its members landed. | |
| 293 | + | /// | |
| 294 | + | /// What `include each` needs: the child numbered its members from zero and | |
| 295 | + | /// they are now at an offset in the parent, so every bound moves by the | |
| 296 | + | /// same amount. The child's scope is its own, so nothing else changes. | |
| 297 | + | fn absorb(&mut self, marks: &Marks, at: usize) { | |
| 298 | + | for mark in marks.all() { | |
| 299 | + | self.mark(Mark { | |
| 300 | + | from: mark.from + at, | |
| 301 | + | to: mark.to + at, | |
| 302 | + | ..*mark | |
| 303 | + | }); | |
| 304 | + | } | |
| 305 | + | } | |
| 306 | + | } | |
| 307 | + | ||
| 308 | + | /// One mark, resolved to the bytes the renderer wrote for it. | |
| 309 | + | /// | |
| 310 | + | /// What a [`Mark`] becomes once something has drawn it: the same site, and the | |
| 311 | + | /// span of output it covers. This is renderer-agnostic on purpose -- a terminal | |
| 312 | + | /// or an immediate renderer that wanted a compiled path would record the same | |
| 313 | + | /// thing over its own output. | |
| 314 | + | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 315 | + | pub struct Cover { | |
| 316 | + | /// The site, as the mark named it. | |
| 317 | + | pub scope: u32, | |
| 318 | + | /// The site, as the mark named it. | |
| 319 | + | pub id: u16, | |
| 320 | + | /// What a request decides about it. | |
| 321 | + | pub varies: Varies, | |
| 322 | + | /// Where the covered markup starts. | |
| 323 | + | pub at: usize, | |
| 324 | + | /// One past where it ends. | |
| 325 | + | pub to: usize, | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | /// A staged twin's answer: what it built, and what varies in it. | |
| 329 | + | /// | |
| 330 | + | /// A twin's return type is free, because nothing but the derivation calls one. | |
| 331 | + | /// That is what lets a shape answering a bare `Vec` be staged at all: the value | |
| 332 | + | /// keeps its own type and the marks ride beside it until the container that | |
| 333 | + | /// splices them in absorbs them. | |
| 334 | + | #[derive(Clone, Debug, Default, PartialEq, Eq)] | |
| 335 | + | pub struct Staged<T> { | |
| 336 | + | /// What the shape built. | |
| 337 | + | pub value: T, | |
| 338 | + | /// What a request decides about it, numbered from this value's own zero. | |
| 339 | + | pub marks: Marks, | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | impl<T> Marking for Staged<Vec<T>> { | |
| 343 | + | /// The members pushed so far. | |
| 344 | + | /// | |
| 345 | + | /// A bare `Vec` is the one container with nowhere to keep a mark, which is | |
| 346 | + | /// what this wrapper is for: the value keeps its own type and the marks | |
| 347 | + | /// ride beside it until whatever holds the list absorbs them. | |
| 348 | + | fn placed(&self) -> usize { | |
| 349 | + | self.value.len() | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | fn mark(&mut self, mark: Mark) { | |
| 353 | + | self.marks.add(mark); | |
| 354 | + | } | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | impl<T> Staged<T> { | |
| 358 | + | /// A value with nothing varying in it. | |
| 359 | + | #[must_use] | |
| 360 | + | pub const fn plain(value: T) -> Self { | |
| 361 | + | Self { | |
| 362 | + | value, | |
| 363 | + | marks: Marks::none(), | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | /// A value and the marks that were made in it. | |
| 368 | + | #[must_use] | |
| 369 | + | pub const fn new(value: T, marks: Marks) -> Self { | |
| 370 | + | Self { value, marks } | |
| 371 | + | } | |
| 372 | + | } | |
| 373 | + | ||
| 195 | 374 | /// Where a plan writes down what it was asked, when anyone is listening. | |
| 196 | 375 | #[derive(Clone, Default)] | |
| 197 | 376 | struct Trace(Option<Rc<RefCell<Sites>>>); |
| @@ -2675,7 +2675,19 @@ | |||
| 2675 | 2675 | content: Markup(value), | |
| 2676 | 2676 | }) | |
| 2677 | 2677 | .collect(); | |
| 2678 | - | cells_html_into(columns, &emitted, opts, out); | |
| 2678 | + | let mut marked = crate::stage::Cursor::open(&cells.marks); | |
| 2679 | + | if marked.watching() { | |
| 2680 | + | // Told where each block went rather than looking for it. A cell that | |
| 2681 | + | // renders to nothing cannot be searched for, and a cell whose markup | |
| 2682 | + | // repeats in the same row would be found twice. | |
| 2683 | + | let mut placed = Vec::new(); | |
| 2684 | + | makeover_webview::list::cells_html_placed(columns, &emitted, opts, out, &mut placed); | |
| 2685 | + | for block in &placed { | |
| 2686 | + | marked.wrote(block.start, block.end); | |
| 2687 | + | } | |
| 2688 | + | } else { | |
| 2689 | + | cells_html_into(columns, &emitted, opts, out); | |
| 2690 | + | } | |
| 2679 | 2691 | ||
| 2680 | 2692 | // After the cells, and outside the grid: a menu is not a value in the row, | |
| 2681 | 2693 | // so it takes no column and the head emits nothing to match it. That is the | |
| @@ -2698,11 +2710,14 @@ | |||
| 2698 | 2710 | class_attr(&["table-row-menu"], opts, out); | |
| 2699 | 2711 | out.push_str(" data-menu=\"row\" hidden>"); | |
| 2700 | 2712 | for act in &cells.menu { | |
| 2713 | + | marked.starts(out); | |
| 2701 | 2714 | act_html(act, opts, &Doc::bare(), out); | |
| 2715 | + | marked.ends(out); | |
| 2702 | 2716 | } | |
| 2703 | 2717 | out.push_str("</div>"); | |
| 2704 | 2718 | } | |
| 2705 | 2719 | ||
| 2720 | + | marked.close(&cells.marks); | |
| 2706 | 2721 | out.push_str(RowLook::Table.close_tag()); | |
| 2707 | 2722 | } | |
| 2708 | 2723 | ||
| @@ -4266,7 +4281,9 @@ | |||
| 4266 | 4281 | // element it is. The wrapper appears only here, so a region that shows | |
| 4267 | 4282 | // everything -- which is every region written before `Showing` existed | |
| 4268 | 4283 | // -- emits exactly the markup it always did. | |
| 4284 | + | let mut marked = crate::stage::Cursor::open(&slot.marks); | |
| 4269 | 4285 | for (at, placed) in slot.body.iter().enumerate() { | |
| 4286 | + | marked.starts(out); | |
| 4270 | 4287 | out.push_str("<div"); | |
| 4271 | 4288 | frame_attrs(slot, at, current == Some(at), opts, out); | |
| 4272 | 4289 | out.push('>'); | |
| @@ -4291,7 +4308,9 @@ | |||
| 4291 | 4308 | _ => ranked_html(placed, opts, doc, out), | |
| 4292 | 4309 | } | |
| 4293 | 4310 | out.push_str("</div>"); | |
| 4311 | + | marked.ends(out); | |
| 4294 | 4312 | } | |
| 4313 | + | marked.close(&slot.marks); | |
| 4295 | 4314 | ||
| 4296 | 4315 | if !labelled { | |
| 4297 | 4316 | run_html(slot, true, opts, doc, out); | |
| @@ -4301,9 +4320,13 @@ | |||
| 4301 | 4320 | repeating_html(slot, repeating, opts, doc, out); | |
| 4302 | 4321 | } else { | |
| 4303 | 4322 | run_html(slot, false, opts, doc, out); | |
| 4323 | + | let mut marked = crate::stage::Cursor::open(&slot.marks); | |
| 4304 | 4324 | for placed in slot.body.iter() { | |
| 4325 | + | marked.starts(out); | |
| 4305 | 4326 | ranked_html(placed, opts, doc, out); | |
| 4327 | + | marked.ends(out); | |
| 4306 | 4328 | } | |
| 4329 | + | marked.close(&slot.marks); | |
| 4307 | 4330 | } | |
| 4308 | 4331 | ||
| 4309 | 4332 | // The host's markup for this region, after whatever the description put |
| @@ -30,11 +30,156 @@ | |||
| 30 | 30 | //! would be reporting a structure the screen does not have. It stops instead. | |
| 31 | 31 | ||
| 32 | 32 | use quasi_http::Serves as _; | |
| 33 | - | use quasi_router::stage::{Op, Plan, Residual, find_sentinel, read_sentinel}; | |
| 33 | + | use quasi_router::stage::{Cover, Op, Plan, Residual, Varies, find_sentinel, read_sentinel}; | |
| 34 | 34 | use quasi_router::{Node, stage}; | |
| 35 | 35 | ||
| 36 | 36 | use crate::Webview; | |
| 37 | 37 | ||
| 38 | + | /// Where a container's members landed in the output, while one is being staged. | |
| 39 | + | /// | |
| 40 | + | /// # Why this is a thread-local and not a parameter | |
| 41 | + | /// | |
| 42 | + | /// The renderer's functions take `opts: &Emit`, and `Emit` is | |
| 43 | + | /// `makeover_webview`'s: a published crate that knows nothing about staging and | |
| 44 | + | /// should not learn. The alternative is a second parameter on every rendering | |
| 45 | + | /// function in `node.rs`, which is over a hundred signatures and every call | |
| 46 | + | /// site, to carry a value that is `None` on every request this server answers. | |
| 47 | + | /// | |
| 48 | + | /// Staging is a build-time pass over one shape at a time, entered by | |
| 49 | + | /// [`derive`] and left before it returns, so there is no second thread and no | |
| 50 | + | /// re-entry. Serving reads the flag once per marked container and finds it | |
| 51 | + | /// empty. | |
| 52 | + | mod recording { | |
| 53 | + | use std::cell::RefCell; | |
| 54 | + | ||
| 55 | + | use quasi_router::stage::{Cover, Mark, Marks}; | |
| 56 | + | ||
| 57 | + | thread_local! { | |
| 58 | + | static COVERS: RefCell<Option<Vec<Cover>>> = const { RefCell::new(None) }; | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | /// Record covers for the duration of one derivation. | |
| 62 | + | pub(super) fn during<R>(body: impl FnOnce() -> R) -> (R, Vec<Cover>) { | |
| 63 | + | COVERS.with(|covers| *covers.borrow_mut() = Some(Vec::new())); | |
| 64 | + | let answer = body(); | |
| 65 | + | let covers = COVERS.with(|covers| covers.borrow_mut().take()); | |
| 66 | + | ( | |
| 67 | + | answer, | |
| 68 | + | covers.expect("the derivation took its own recorder away"), | |
| 69 | + | ) | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | /// Whether anything is listening. | |
| 73 | + | fn listening() -> bool { | |
| 74 | + | COVERS.with(|covers| covers.borrow().is_some()) | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | fn record(cover: Cover) { | |
| 78 | + | COVERS.with(|covers| { | |
| 79 | + | if let Some(covers) = covers.borrow_mut().as_mut() { | |
| 80 | + | covers.push(cover); | |
| 81 | + | } | |
| 82 | + | }); | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | /// Where one container's members landed, as they are written. | |
| 86 | + | /// | |
| 87 | + | /// Nothing at all when no derivation is running or the container carries no | |
| 88 | + | /// marks, which is every container on the serving path. | |
| 89 | + | /// | |
| 90 | + | /// Members are recorded as ranges rather than as boundaries because they | |
| 91 | + | /// are not always adjacent: a row's cells are written back to back, a | |
| 92 | + | /// region's members have the frame wrapper between them, and a menu comes | |
| 93 | + | /// after markup that belongs to neither. A range says what one member wrote | |
| 94 | + | /// and claims nothing about the gaps. | |
| 95 | + | pub struct Cursor(Option<Vec<(usize, usize)>>); | |
| 96 | + | ||
| 97 | + | impl Cursor { | |
| 98 | + | /// Start watching a container's members. | |
| 99 | + | #[must_use] | |
| 100 | + | pub fn open(marks: &Marks) -> Self { | |
| 101 | + | Self((!marks.is_empty() && listening()).then(Vec::new)) | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | /// Whether anything is being recorded for this container. | |
| 105 | + | /// | |
| 106 | + | /// For a caller whose two ways of writing a container differ in cost: | |
| 107 | + | /// asking `makeover-webview` to say where every cell went is a `Vec` | |
| 108 | + | /// per row, and a request should not pay for one. | |
| 109 | + | #[must_use] | |
| 110 | + | pub const fn watching(&self) -> bool { | |
| 111 | + | self.0.is_some() | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | /// A member is about to be written. | |
| 115 | + | pub fn starts(&mut self, out: &str) { | |
| 116 | + | if let Some(members) = self.0.as_mut() { | |
| 117 | + | members.push((out.len(), out.len())); | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | /// The member that was being written is finished. | |
| 122 | + | pub fn ends(&mut self, out: &str) { | |
| 123 | + | if let Some(members) = self.0.as_mut() | |
| 124 | + | && let Some(last) = members.last_mut() | |
| 125 | + | { | |
| 126 | + | last.1 = out.len(); | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | ||
| 130 | + | /// A member whose bounds the writer already knows. | |
| 131 | + | /// | |
| 132 | + | /// For a container that hands its assembly to `makeover-webview` and is | |
| 133 | + | /// told afterwards where each piece went. | |
| 134 | + | pub fn wrote(&mut self, at: usize, to: usize) { | |
| 135 | + | if let Some(members) = self.0.as_mut() { | |
| 136 | + | members.push((at, to)); | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | /// The container is finished: turn its marks into covers. | |
| 141 | + | /// | |
| 142 | + | /// A mark names members and the record says what each one wrote, so | |
| 143 | + | /// `from..to` is the start of the first and the end of the last, and | |
| 144 | + | /// nothing is searched for. A mark naming a member the container did | |
| 145 | + | /// not draw is a twin that marked what it did not emit, which is a bug | |
| 146 | + | /// rather than something to paper over, so it says so. | |
| 147 | + | pub fn close(self, marks: &Marks) { | |
| 148 | + | let Some(members) = self.0 else { | |
| 149 | + | return; | |
| 150 | + | }; | |
| 151 | + | for &Mark { | |
| 152 | + | scope, | |
| 153 | + | id, | |
| 154 | + | varies, | |
| 155 | + | from, | |
| 156 | + | to, | |
| 157 | + | } in marks.all() | |
| 158 | + | { | |
| 159 | + | assert!( | |
| 160 | + | to > from, | |
| 161 | + | "mark {scope:x}/{id} covers no member at all ({from}..{to})" | |
| 162 | + | ); | |
| 163 | + | let (Some(first), Some(last)) = (members.get(from), members.get(to - 1)) else { | |
| 164 | + | panic!( | |
| 165 | + | "mark {scope:x}/{id} covers members {from}..{to} and the container drew {}", | |
| 166 | + | members.len() | |
| 167 | + | ); | |
| 168 | + | }; | |
| 169 | + | record(Cover { | |
| 170 | + | scope, | |
| 171 | + | id, | |
| 172 | + | varies, | |
| 173 | + | at: first.0, | |
| 174 | + | to: last.1, | |
| 175 | + | }); | |
| 176 | + | } | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | ||
| 181 | + | pub use recording::Cursor; | |
| 182 | + | ||
| 38 | 183 | /// What a request's value does on its way into a residual's gap. | |
| 39 | 184 | /// | |
| 40 | 185 | /// A small closed set rather than a blanket over `Display`, for two reasons. | |
| @@ -95,496 +240,144 @@ | |||
| 95 | 240 | u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, f32, f64, bool, char | |
| 96 | 241 | ); | |
| 97 | 242 | ||
| 98 | - | /// A span of the base render, and what decides it. | |
| 243 | + | /// A run of the base render, and what a request decides about it. | |
| 99 | 244 | /// | |
| 100 | - | /// `at` is where the span is finally placed; `first` is the earliest placement | |
| 101 | - | /// that is equally consistent with the two renders it was read from. See | |
| 102 | - | /// [`between`] for why a span has a range at all, and [`placed`] for what picks | |
| 103 | - | /// one point in it. | |
| 245 | + | /// Read off a [`Cover`] the renderer recorded, so `at` and `to` are where the | |
| 246 | + | /// markup was written and not where a comparison guessed it might be. | |
| 104 | 247 | #[derive(Clone, PartialEq, Eq, Debug)] | |
| 105 | 248 | struct Span { | |
| 106 | 249 | at: usize, | |
| 107 | 250 | to: usize, | |
| 108 | - | repeats: bool, | |
| 109 | - | /// The earliest `at` that rebuilds the same string. `at` is the latest. | |
| 110 | - | first: usize, | |
| 111 | - | /// What each of a dispatch's other cases puts in this range. | |
| 251 | + | varies: Varies, | |
| 252 | + | /// The other arms of a dispatch, already built, in declaration order. | |
| 112 | 253 | /// | |
| 113 | - | /// Empty for everything that is not a dispatch. The base render holds the | |
| 114 | - | /// first case, so these are cases 1..n in declaration order. | |
| 115 | - | cases: Vec<String>, | |
| 116 | - | /// What stands in this range when the guard does not pass. | |
| 117 | - | /// | |
| 118 | - | /// `None` for every span that is a gap: a branch is markup that is there or | |
| 119 | - | /// is not, and a loop is a body that repeats. `Some` is a SWAP, where the | |
| 120 | - | /// render with the guard off holds different markup rather than less of it, | |
| 121 | - | /// and the two together become [`Op::Arms`]. See [`swapped`]. | |
| 122 | - | other: Option<String>, | |
| 254 | + | /// Empty for a guard and for a loop. The base render holds arm 0, so these | |
| 255 | + | /// are arms 1..n, each built from its own render and therefore holding | |
| 256 | + | /// whatever structure is inside it. | |
| 257 | + | arms: Vec<Vec<Op>>, | |
| 123 | 258 | } | |
| 124 | 259 | ||
| 125 | 260 | /// The residual of one staged shape. | |
| 126 | 261 | /// | |
| 127 | 262 | /// `shape` is the staged twin, which takes a plan and answers a `Node`. It is | |
| 128 | - | /// called many times and must be free of side effects, which it is: a staged | |
| 129 | - | /// twin reads nothing but its plan. | |
| 263 | + | /// called once, plus once more per arm of each dispatch, and must be free of | |
| 264 | + | /// side effects, which it is: a staged twin reads nothing but its plan. | |
| 265 | + | /// | |
| 266 | + | /// # How a boundary is found | |
| 267 | + | /// | |
| 268 | + | /// It is not found. The twin marks the members each guard, loop and dispatch | |
| 269 | + | /// covers, the renderer records where it wrote them, and this reads the record. | |
| 270 | + | /// Nothing here compares two renders, measures a common prefix, or decides | |
| 271 | + | /// where a deletion "would have" gone. | |
| 272 | + | /// | |
| 273 | + | /// A dispatch is the one site that still renders more than once, and for a | |
| 274 | + | /// reason that is not about finding anything: the arm a request does not take | |
| 275 | + | /// was never drawn, so there is nothing to compile until it is. Each arm is | |
| 276 | + | /// rendered under its own plan and read off its own cover, and because the | |
| 277 | + | /// cover is exact, an arm may hold loops and guards of its own. | |
| 130 | 278 | /// | |
| 131 | 279 | /// # Panics | |
| 132 | 280 | /// | |
| 133 | - | /// If the screen's branches and loops do not nest, or if a span does not put | |
| 134 | - | /// back the way it came out. Both mean the derivation has read a structure the | |
| 135 | - | /// screen does not have, and a residual built on one would serve markup that is | |
| 136 | - | /// quietly wrong. Loud is the only safe answer. | |
| 281 | + | /// If the marked runs do not nest, if an arm's render differs outside the arm, | |
| 282 | + | /// or if a mark names a member its container did not draw. Each means the tree | |
| 283 | + | /// and the markup disagree about the screen's structure, and a residual built | |
| 284 | + | /// on one would serve markup that is quietly wrong. | |
| 137 | 285 | #[must_use] | |
| 138 | 286 | pub fn derive<F>(webview: &Webview, shape: F) -> Residual | |
| 139 | 287 | where | |
| 140 | 288 | F: Fn(&Plan) -> Node, | |
| 141 | 289 | { | |
| 142 | - | let render = |plan: &Plan| webview.fragment(&shape(plan)); | |
| 290 | + | let plan = Plan::full(1); | |
| 291 | + | let render = |plan: &Plan| recording::during(|| webview.fragment(&shape(plan))); | |
| 143 | 292 | ||
| 144 | - | // One row per loop and every guard passing. A guard that does not pass | |
| 145 | - | // places nothing, so a residual read off a plan that failed one would be | |
| 146 | - | // missing a branch rather than carrying it. | |
| 147 | - | let base_plan = Plan::full(1); | |
| 148 | - | let base = render(&base_plan); | |
| 149 | - | ||
| 150 | - | // One traced render names every guard and every loop the screen reaches, | |
| 151 | - | // so the spans below are read for sites that are known to exist rather than | |
| 152 | - | // hunted for. A shape carrying only markup and a loop leaves no sentinel to | |
| 153 | - | // be found by, which is why this is a record and not a scan of the output. | |
| 154 | - | let (traced, sites) = base_plan.clone().tracing(); | |
| 155 | - | let _ = render(&traced); | |
| 156 | - | let sites = sites.borrow(); | |
| 293 | + | // Every guard passes and every loop runs once, so the base render holds one | |
| 294 | + | // of everything the screen can draw. A guard that did not pass would leave | |
| 295 | + | // a residual with no branch to fill. | |
| 296 | + | let (base, covers) = render(&plan); | |
| 157 | 297 | ||
| 158 | 298 | let mut spans = Vec::new(); | |
| 159 | - | for &(scope, id) in &sites.loops { | |
| 160 | - | let wider = render(&base_plan.clone().with_rows_at(scope, id, 2)); | |
| 161 | - | let Some(span) = grown(&base, &wider) else { | |
| 162 | - | unheld("loop", scope, id, &base, &wider); | |
| 299 | + | for cover in &covers { | |
| 300 | + | let arms = match cover.varies { | |
| 301 | + | Varies::Arm { of, at: 0 } => other_arms(&render, &plan, &base, cover, of), | |
| 302 | + | // Arms past the first are read from their own render, below the one | |
| 303 | + | // that names them, so they are not spans of the base at all. | |
| 304 | + | Varies::Arm { .. } => continue, | |
| 305 | + | Varies::Absent | Varies::Repeated => Vec::new(), | |
| 163 | 306 | }; | |
| 164 | 307 | spans.push(Span { | |
| 165 | - | repeats: true, | |
| 166 | - | ..span | |
| 308 | + | at: cover.at, | |
| 309 | + | to: cover.to, | |
| 310 | + | varies: cover.varies, | |
| 311 | + | arms, | |
| 167 | 312 | }); | |
| 168 | 313 | } | |
| 169 | - | let marks = sentinels(&base); | |
| 170 | - | // A dispatch is arms by construction rather than by discovery: its cases | |
| 171 | - | // replace each other at one position, which is what `Op::Arms` is. Rendered | |
| 172 | - | // once per case, and the region they disagree over is the arms. | |
| 173 | - | for &(scope, id, count) in &sites.arms { | |
| 174 | - | let others = (1..count) | |
| 175 | - | .map(|arm| render(&base_plan.clone().with_arm_at(scope, id, arm))) | |
| 176 | - | .collect::<Vec<_>>(); | |
| 177 | - | let Some(span) = dispatched(&base, &others, &marks) else { | |
| 178 | - | unheld( | |
| 179 | - | "dispatch", | |
| 180 | - | scope, | |
| 181 | - | id, | |
| 182 | - | &base, | |
| 183 | - | others.first().map_or(&base, |first| first), | |
| 184 | - | ); | |
| 185 | - | }; | |
| 186 | - | spans.push(span); | |
| 187 | - | } | |
| 188 | - | for &(scope, id) in &sites.guards { | |
| 189 | - | let without = render(&base_plan.clone().with_guard_at(scope, id, false)); | |
| 190 | - | let span = match shrunk(&base, &without) { | |
| 191 | - | Some(span) => Span { | |
| 192 | - | repeats: false, | |
| 193 | - | ..span | |
| 194 | - | }, | |
| 195 | - | // Not a deletion. It may still be a SWAP -- markup replaced rather | |
| 196 | - | // than removed -- which is a shape the residual has since | |
| 197 | - | // `Op::Arms`. Anything else is refused. | |
| 198 | - | None => match swapped(&base, &without, &marks) { | |
| 199 | - | Some(span) => span, | |
| 200 | - | None => unheld("guard", scope, id, &base, &without), | |
| 201 | - | }, | |
| 202 | - | }; | |
| 203 | - | spans.push(span); | |
| 204 | - | } | |
| 205 | 314 | ||
| 206 | 315 | Residual::new(tree(&base, &mut spans)) | |
| 207 | 316 | } | |
| 208 | 317 | ||
| 209 | - | /// Refuse a site the residual has no shape for, and say which kind it is. | |
| 318 | + | /// Every arm of one dispatch but the first, compiled from its own render. | |
| 210 | 319 | /// | |
| 211 | - | /// Reached when varying one guard or one loop does not change the render the | |
| 212 | - | /// way a gap changes it. Three things can be behind that, and the message | |
| 213 | - | /// separates them because the fix is different for each. | |
| 320 | + | /// The base render drew arm 0. Each of the others is drawn under a plan that | |
| 321 | + | /// asks for it, and its cover says exactly which bytes it wrote -- so the arm | |
| 322 | + | /// is compiled the same way the whole screen is, with its own marks inside it. | |
| 214 | 323 | /// | |
| 215 | - | /// # Why this is loud here rather than left to the filler | |
| 324 | + | /// # What is checked, and why it is fatal | |
| 216 | 325 | /// | |
| 217 | - | /// The filler already refuses the result: a residual with no branch where the | |
| 218 | - | /// fill program has a guard panics in [`Cursor::branch`]. But it refuses it on a | |
| 219 | - | /// REQUEST, so a screen without a filling test ships and takes its first reader | |
| 220 | - | /// down, and it can be quiet instead -- two sites that both fail this way and | |
| 221 | - | /// whose bodies happen to have the same shape line up, and the filler answers | |
| 222 | - | /// one site's branch with the other's arm. Refusing here moves both cases to the | |
| 223 | - | /// build machine, where the residual is generated. | |
| 224 | - | /// | |
| 225 | - | /// The commonest cause by a distance is a **substitution**: markup that replaces | |
| 226 | - | /// other markup rather than being absent. A guard derives as a branch only when | |
| 227 | - | /// turning it off deletes bytes, so a renderer that draws one thing when a | |
| 228 | - | /// value is present and a different thing when it is not gives the derivation | |
| 229 | - | /// nothing to measure. `rest_html` drew a disabled control for a pager direction | |
| 230 | - | /// with no address until 2026-09-08, and `rest_strip_html` still draws the page | |
| 231 | - | /// a reader is on as a readout rather than a control. Both are two markups at | |
| 232 | - | /// one position, which is what `Op::Arms` is for. | |
| 233 | - | #[cold] | |
| 234 | - | fn unheld(kind: &str, scope: u32, id: u16, base: &str, varied: &str) -> ! { | |
| 235 | - | let what = if base == varied { | |
| 236 | - | "it renders exactly the same either way, so it places nothing and the \ | |
| 237 | - | residual has no gap to hold. A member that is sometimes nothing is a \ | |
| 238 | - | member whose value is sometimes empty, and empty is a different shape \ | |
| 239 | - | rather than a shorter one" | |
| 240 | - | } else if base.len() == varied.len() { | |
| 241 | - | "the two renders are the same length and differ in their bytes, which \ | |
| 242 | - | is a substitution rather than a gap: the screen is saying two markups \ | |
| 243 | - | at one position" | |
| 244 | - | } else { | |
| 245 | - | "what changes is not a clean insertion or deletion, so it is a \ | |
| 246 | - | substitution rather than a gap: the screen is saying two markups at \ | |
| 247 | - | one position, and a residual holds one" | |
| 248 | - | }; | |
| 249 | - | panic!( | |
| 250 | - | "this screen cannot be staged: {kind} {id} in scope {scope} varies the \ | |
| 251 | - | render in a way a residual cannot hold, because {what}.\n\nWith the \ | |
| 252 | - | {kind} on, the render is {} bytes; with it off, {}.", | |
| 253 | - | base.len(), | |
| 254 | - | varied.len(), | |
| 255 | - | ); | |
| 326 | + | /// That the two renders agree everywhere outside the arm. A dispatch is two | |
| 327 | + | /// markups at ONE position; if picking a different arm also changed something | |
| 328 | + | /// elsewhere, the residual would carry one arm's version of that change for | |
| 329 | + | /// every request, whichever arm it later took. | |
| 330 | + | fn other_arms<R>(render: &R, plan: &Plan, base: &str, cover: &Cover, of: usize) -> Vec<Vec<Op>> | |
| 331 | + | where | |
| 332 | + | R: Fn(&Plan) -> (String, Vec<Cover>), | |
| 333 | + | { | |
| 334 | + | let mut arms = Vec::with_capacity(of.saturating_sub(1)); | |
| 335 | + | for arm in 1..of { | |
| 336 | + | let (drawn, covers) = render(&plan.clone().with_arm_at(cover.scope, cover.id, arm)); | |
| 337 | + | let Some(here) = covers | |
| 338 | + | .iter() | |
| 339 | + | .find(|other| other.scope == cover.scope && other.id == cover.id) | |
| 340 | + | else { | |
| 341 | + | panic!( | |
| 342 | + | "dispatch {:x}/{} drew arm {arm} nowhere, so the screen has an arm \ | |
| 343 | + | that no request can reach", | |
| 344 | + | cover.scope, cover.id | |
| 345 | + | ); | |
| 346 | + | }; | |
| 347 | + | assert!( | |
| 348 | + | base[..cover.at] == drawn[..here.at] && base[cover.to..] == drawn[here.to..], | |
| 349 | + | "dispatch {:x}/{} arm {arm} changed markup outside its own position, \ | |
| 350 | + | so it is not two markups at one place", | |
| 351 | + | cover.scope, | |
| 352 | + | cover.id | |
| 353 | + | ); | |
| 354 | + | let mut inside = spans_within(&covers, here); | |
| 355 | + | arms.push(build(&drawn, here.at, here.to, &mut inside, &mut 0)); | |
| 356 | + | } | |
| 357 | + | arms | |
| 256 | 358 | } | |
| 257 | 359 | ||
| 258 | - | /// The span `wide` holds one more time than `narrow` does, in `narrow`. | |
| 360 | + | /// The marked runs strictly inside one cover, as spans of that render. | |
| 259 | 361 | /// | |
| 260 | - | /// `narrow` is `P R Q` and `wide` is `P R R Q`, so the extra copy is the length | |
| 261 | - | /// difference. [`between`] finds where that copy sits in `wide`, which is the | |
| 262 | - | /// SECOND one; the body's own span in `narrow` is the first, ending where the | |
| 263 | - | /// two renders stop agreeing. Getting that conversion wrong is what put a | |
| 264 | - | /// table's rows halfway down the next section. | |
| 265 | - | /// | |
| 266 | - | /// Checked by taking the copy back out of `wide`. The two copies are the same | |
| 267 | - | /// string, so removing either gives `narrow` back, which is what makes the | |
| 268 | - | /// check a check on the length and the position rather than on which one. | |
| 269 | - | fn grown(narrow: &str, wide: &str) -> Option<Span> { | |
| 270 | - | let length = wide.len().checked_sub(narrow.len())?; | |
| 271 | - | if length == 0 { | |
| 272 | - | return None; | |
| 273 | - | } | |
| 274 | - | let found = between(narrow, wide, length)?; | |
| 275 | - | let mut rebuilt = String::with_capacity(narrow.len()); | |
| 276 | - | rebuilt.push_str(&wide[..found.at]); | |
| 277 | - | rebuilt.push_str(&wide[found.at + length..]); | |
| 278 | - | if rebuilt != narrow { | |
| 279 | - | return None; | |
| 280 | - | } | |
| 281 | - | ||
| 282 | - | let at = found.at.checked_sub(length)?; | |
| 283 | - | let to = found.at; | |
| 284 | - | // The two placements convert differently, and that is the whole of this | |
| 285 | - | // conversion rather than an optimisation of it. | |
| 286 | - | // | |
| 287 | - | // `wide` is `P F F Q` and the removable copy sits anywhere from `|P|` to | |
| 288 | - | // `|P| + |F|`. The LATEST of those is one copy after the body's own place, | |
| 289 | - | // so it converts by subtracting a body length. The EARLIEST is already the | |
| 290 | - | // body's own place -- `|P|` is where the first copy starts in `wide` and in | |
| 291 | - | // `narrow` alike -- so it converts by not moving at all. | |
| 292 | - | // | |
| 293 | - | // Subtracting from both is what this did until 2026-09-08 and it was wrong | |
| 294 | - | // twice over. It underflowed whenever the body was longer than the markup | |
| 295 | - | // before it, which a table whose rows outrun its heading row always is; | |
| 296 | - | // that was read as `?`, answered `None`, and dropped the loop, leaving the | |
| 297 | - | // residual holding one pass as a literal while the fill program still had | |
| 298 | - | // its `Repeat`. Answering `at` instead stopped the drop and left a window | |
| 299 | - | // one point wide, which is the second failure: `placed` had nowhere to | |
| 300 | - | // slide to. | |
| 301 | - | // | |
| 302 | - | // That matters because the prefix scan can run PAST the body. `</div>` and | |
| 303 | - | // `<div class="figure"` share their `<`, so the latest placement is one | |
| 304 | - | // byte beyond the copy and the converted `at` is one byte inside the | |
| 305 | - | // opening tag -- markup no emission produced. With a window that reaches | |
| 306 | - | // back to `first`, `placed` corrects it; without one, the loop sat a byte | |
| 307 | - | // off its own body and overlapped a guard inside it, which `build` then | |
| 308 | - | // refused as a structure that is not a tree. A guarded figure in a stats | |
| 309 | - | // strip is the site: MNW's `/dashboard/tabs/analytics`. | |
| 310 | - | let first = found.first.min(at); | |
| 311 | - | (narrow.is_char_boundary(at) && narrow.is_char_boundary(to)).then_some(Span { | |
| 312 | - | at, | |
| 313 | - | to, | |
| 314 | - | repeats: true, | |
| 315 | - | first, | |
| 316 | - | other: None, | |
| 317 | - | cases: Vec::new(), | |
| 318 | - | }) | |
| 319 | - | } | |
| 320 | - | ||
| 321 | - | /// The span `full` holds and `without` does not. | |
| 322 | - | /// | |
| 323 | - | /// The same arithmetic the other way round: a guard that stops passing deletes | |
| 324 | - | /// what it placed, and the deletion is between the common prefix and the common | |
| 325 | - | /// suffix. Checked by taking it back out of `full`. | |
| 326 | - | fn shrunk(full: &str, without: &str) -> Option<Span> { | |
| 327 | - | let length = full.len().checked_sub(without.len())?; | |
| 328 | - | if length == 0 { | |
| 329 | - | return None; | |
| 330 | - | } | |
| 331 | - | let span = between(without, full, length)?; | |
| 332 | - | let mut rebuilt = String::with_capacity(without.len()); | |
| 333 | - | rebuilt.push_str(&full[..span.at]); | |
| 334 | - | rebuilt.push_str(&full[span.at + length..]); | |
| 335 | - | (rebuilt == without).then_some(span) | |
| 336 | - | } | |
| 337 | - | ||
| 338 | - | /// Where `long` differs from `short`, given that it is longer by `length`. | |
| 339 | - | /// | |
| 340 | - | /// Between the common prefix and the common suffix. Both are needed: a prefix | |
| 341 | - | /// alone cannot tell a body from the text that follows it when the two begin | |
| 342 | - | /// the same way, which is the ambiguity that makes a search necessary and a | |
| 343 | - | /// named site unnecessary. | |
| 344 | - | /// | |
| 345 | - | /// # The deletion is a range, not a point | |
| 346 | - | /// | |
| 347 | - | /// When what is deleted shares a boundary with what follows it, several | |
| 348 | - | /// placements rebuild `short` from `long` **exactly**, so no check on one | |
| 349 | - | /// placement can tell them apart. Two adjacent described regions are the site, | |
| 350 | - | /// and they are not an edge case: every region opens `<section id="` and closes | |
| 351 | - | /// `</section>`, so a pair of guarded siblings always shares a prefix, and MNW's | |
| 352 | - | /// `/fan-plus` shares 22 bytes of it. | |
| 353 | - | /// | |
| 354 | - | /// So this answers with the whole range. `first` is the earliest placement, from |
Lines truncated