max / quasi
| 1 | //! Nodes to markup. |
| 2 | //! |
| 3 | //! Every function here takes a piece of [`quasi_router`]'s screen tree and |
| 4 | //! pushes markup onto a buffer. Nothing returns a `Result`: a description that |
| 5 | //! exists is renderable by construction, which is the property the owned mirror |
| 6 | //! in `quasi-router` was built to have. |
| 7 | //! |
| 8 | //! # Where the htmx goes in |
| 9 | //! |
| 10 | //! In exactly one function, [`action_attrs`]. An [`Action`] is a method, a path |
| 11 | //! and some params, and turning that into `hx-get` / `hx-post` / `hx-vals` is |
| 12 | //! the whole of what "htmx is the transport" means in code. Nothing else in |
| 13 | //! this file knows the word htmx, so decision 13's claim that the transport is |
| 14 | //! replaceable is a claim about one function rather than about the crate. |
| 15 | //! |
| 16 | //! `hx-target` is emitted for one case and only one: [`Action::replaces`], set |
| 17 | //! when a control calls a route the description layer does not serve. Decision 7 |
| 18 | //! puts the target on the response, where `quasi-http` sets `HX-Retarget` from |
| 19 | //! [`Response::Fragment`](quasi_router::Response::Fragment), because the router |
| 20 | //! is the only party that knows what it just changed, and a control that also |
| 21 | //! named a target would be a second party deciding one thing. That reasoning |
| 22 | //! assumes the responder is described. A plain API route is not, cannot name a |
| 23 | //! region, and leaves the answer to land wherever the transport defaults, which |
| 24 | //! for htmx is inside the pressed button. See `Action::replaces` for the whole |
| 25 | //! of it. |
| 26 | |
| 27 | use HashMap; |
| 28 | use Write as _; |
| 29 | |
| 30 | use makeover_layout as layout; |
| 31 | // `Tone::token` is the trait method, and `data-tone` is spelled from it rather |
| 32 | // than from a match here, so a tone added upstream cannot be named two ways. |
| 33 | use Intent as _; |
| 34 | use chart_html_into; |
| 35 | use figure_html_into; |
| 36 | use ; |
| 37 | // `class`, `option_class` and the two part-class mappings below are makeover's, |
| 38 | // not copies of it. They were copies until makeover-webview 0.27.0 made them |
| 39 | // public: the prefix helper was byte-identical, and the row and cell part names |
| 40 | // were a second spelling of a list whose own doc comment carries an obligation |
| 41 | // to be grepped on upgrade. A second spelling is a second place to forget, and |
| 42 | // the selector names had already drifted. |
| 43 | use ; |
| 44 | // `Cell` is a name both crates use: makeover's is the emitted table cell, ours |
| 45 | // is the described one. Aliased rather than qualified at the call site, so the |
| 46 | // two never read as the same type. |
| 47 | use |
| 48 | Cell as Emitted, cell_part_class, cells_html_into, flow_class, part_class, push_column_classes, |
| 49 | ; |
| 50 | use meter_html_into; |
| 51 | use placeholder_html_into; |
| 52 | use |
| 53 | Act, Bar, Cell, Clock, Destination, Field, Node, Repeat, Rest, Row, Slot, Tag, |
| 54 | ; |
| 55 | use ; |
| 56 | |
| 57 | /// The facts about the document being written that no [`Node`] carries. |
| 58 | /// |
| 59 | /// Threaded beside [`Emit`] rather than folded into it: `Emit` is the host's |
| 60 | /// standing configuration and is the same for every answer, while this is about |
| 61 | /// the one answer being written. Two members and both of them are the screen's: |
| 62 | /// what a bespoke region was handed, and where the caret starts. |
| 63 | /// |
| 64 | /// A struct rather than two more parameters. The walk is deep and every level |
| 65 | /// passes it along untouched, so the third document-level fact would otherwise |
| 66 | /// be a third edit to every signature between here and the leaf. |
| 67 | pub |
| 68 | /// The markup a host drew for a region the description ceded, by |
| 69 | /// [`Slot::id`](quasi_router::Slot). |
| 70 | pub(crate) fills: &'a , |
| 71 | /// The [`Field::name`] the caret starts in, when this answer is a whole |
| 72 | /// document and the screen named one. |
| 73 | /// |
| 74 | /// `None` on every fragment, which is the whole of the accessibility |
| 75 | /// argument: a swap that moves the caret takes it out of whatever the |
| 76 | /// reader was typing into, and a browser answers most interactions with a |
| 77 | /// fragment. See [`Screen::opens_at`](quasi_router::Screen::opens_at). |
| 78 | pub(crate) opens_at: , |
| 79 | |
| 80 | |
| 81 | /// The empty fill map, for the answers that cede no region. |
| 82 | /// |
| 83 | /// One allocation for the process rather than one per call. Every site reaching |
| 84 | /// for it is on a path that cannot hold a bespoke region at all, so the map is |
| 85 | /// empty by construction rather than by luck. |
| 86 | static NO_FILLS: LazyLock = |
| 87 | new; |
| 88 | |
| 89 | |
| 90 | /// No fills and no caret, for the fragments and the run-level walks. |
| 91 | pub |
| 92 | Self |
| 93 | fills: &NO_FILLS, |
| 94 | opens_at: None, |
| 95 | |
| 96 | |
| 97 | |
| 98 | |
| 99 | |
| 100 | /// What a host drew, and no caret. |
| 101 | /// |
| 102 | /// Everything but a whole document: a fragment, an overlay, the chrome and |
| 103 | /// the frame. See [`opens_at`](Self::opens_at). |
| 104 | pub |
| 105 | Self |
| 106 | fills, |
| 107 | opens_at: None, |
| 108 | |
| 109 | |
| 110 | |
| 111 | /// What a host drew, and where the caret starts. |
| 112 | pub |
| 113 | Self |
| 114 | |
| 115 | |
| 116 | /// Whether this field is the one the caret starts in. |
| 117 | |
| 118 | self.opens_at == Some |
| 119 | |
| 120 | |
| 121 | |
| 122 | /// Write one prefixed class name onto a buffer the caller already has. |
| 123 | /// |
| 124 | /// The two halves are escaped separately rather than joined and escaped once. |
| 125 | /// That is the same bytes, since escaping is per character and has no context to |
| 126 | /// carry across the seam, and none of the allocations. |
| 127 | /// |
| 128 | /// Escaped at all because a prefix is host configuration reaching an attribute |
| 129 | /// value. It is a `&'static str` and every real one is identity under this, so |
| 130 | /// the cost is a scan; what it buys is that the one string here that did not |
| 131 | /// come from this crate cannot end the attribute. |
| 132 | pub |
| 133 | escape_into; |
| 134 | escape_into; |
| 135 | |
| 136 | |
| 137 | /// Write a `class="..."` attribute, prefixed. |
| 138 | |
| 139 | out.push_str; |
| 140 | for in names.iter.enumerate |
| 141 | if i > 0 |
| 142 | out.push; |
| 143 | |
| 144 | class_into; |
| 145 | |
| 146 | out.push; |
| 147 | |
| 148 | |
| 149 | /// Write the attribute naming a tone, if the tone is worth naming. |
| 150 | /// |
| 151 | /// [`Tone::Neutral`] writes nothing: ordinary content is the default, and an |
| 152 | /// attribute meaning "nothing unusual" is an attribute on every element in the |
| 153 | /// document. |
| 154 | /// |
| 155 | /// An attribute and not a class, which is the correction. This emitted |
| 156 | /// `tone-info`, `tone-success`, `tone-warning` and `tone-danger` as classes, and |
| 157 | /// makeover has never defined one of them: its whole vocabulary keys tone off |
| 158 | /// `data-tone`, from `.badge[data-tone="danger"]` to the progress fill to a |
| 159 | /// figure's value. So every toned thing a description produced arrived with a |
| 160 | /// class no stylesheet in the tree had heard of, which is why the SSH-keys tab's |
| 161 | /// Remove button came out the same colour as everything else. |
| 162 | |
| 163 | if matches! |
| 164 | return; |
| 165 | |
| 166 | out.push_str; |
| 167 | out.push_str; |
| 168 | out.push; |
| 169 | |
| 170 | |
| 171 | /// How much of its row a control asked for, where it is not the default. |
| 172 | /// |
| 173 | /// `tone_attr`'s shape and its reason. An attribute rather than a class because |
| 174 | /// this is a fact the description carried, not a styling hook this renderer |
| 175 | /// invented -- the same division `data-selector` and `data-tone` are on the |
| 176 | /// right side of. `Fill` is the default and is what a control did before the |
| 177 | /// member existed, so saying it would be a hook that changes nothing. |
| 178 | |
| 179 | let said = match width |
| 180 | Content => "content", |
| 181 | Fixed => "fixed", |
| 182 | // `Fill` is the default, and a member added upstream since this |
| 183 | // renderer learned the vocabulary is one it does not draw. |
| 184 | _ => return, |
| 185 | ; |
| 186 | out.push_str; |
| 187 | out.push_str; |
| 188 | out.push; |
| 189 | |
| 190 | |
| 191 | /// How a picture sits in its box, where it is not the default. |
| 192 | /// |
| 193 | /// `tone_attr`'s shape and for its reason: `Natural` is what an `<img>` does |
| 194 | /// with no rule at all, so saying it would be a stylesheet hook that changes |
| 195 | /// nothing. The two that need a rule get one. |
| 196 | |
| 197 | let value = match fit |
| 198 | Natural => return, |
| 199 | Cover => "cover", |
| 200 | Contain => "contain", |
| 201 | // `Fit` is `#[non_exhaustive]`, so a member added upstream lands here |
| 202 | // rather than failing the build. Drawing it as natural is the safe |
| 203 | // read: the picture is whole and its own shape, which is wrong about |
| 204 | // the box and never wrong about the content. |
| 205 | _ => return, |
| 206 | ; |
| 207 | out.push_str; |
| 208 | out.push_str; |
| 209 | out.push; |
| 210 | |
| 211 | |
| 212 | /// What goes back in the box, for a form being offered again after a refusal. |
| 213 | /// |
| 214 | /// The description carries the value as a string, because that is what came |
| 215 | /// off the wire; the kind is what says how to read it. A checkbox is carried |
| 216 | /// by presence the way HTML submits one, so any value means ticked and nothing |
| 217 | /// means not. |
| 218 | /// |
| 219 | /// A [`layout::FieldKind::Secret`] is emitted empty whatever it holds. That is |
| 220 | /// the third refusal of the same thing and none of the three is redundant: |
| 221 | /// `Field::value` will not store one, `makeover_webview::form` will not write |
| 222 | /// one into an `<input type="password">`, and this one stands between them |
| 223 | /// because `Field::value` is a public field that a struct literal reaches past. |
| 224 | |
| 225 | if field.kind == Secret |
| 226 | return Absent; |
| 227 | |
| 228 | // An interval came back under two names, so it goes back into two boxes. |
| 229 | // Either end absent is an answer rather than a half-filled form -- "over |
| 230 | // 120 BPM" -- so this is not gated on both being present. |
| 231 | if field.kind == Interval |
| 232 | return Between |
| 233 | lower: field.value.as_deref.unwrap_or_default, |
| 234 | upper: field.upper_value.as_deref.unwrap_or_default, |
| 235 | ; |
| 236 | |
| 237 | match field.value.as_deref |
| 238 | None => Absent, |
| 239 | Some if field.kind == Checkbox => On, |
| 240 | Some => Text, |
| 241 | |
| 242 | |
| 243 | |
| 244 | /// Whether the reader reaches this control's value by typing characters into |
| 245 | /// it. |
| 246 | /// |
| 247 | /// What `keyup` can hear, which is the question the two consult triggers turn |
| 248 | /// on: a `<select>`, a radio, a checkbox, a slider and a date picker all |
| 249 | /// change without a key ever being released over them, so a question hung on |
| 250 | /// `keyup` is one they never ask. |
| 251 | /// |
| 252 | /// Not [`layout::FieldKind::offers_options`] and its two companions, which the |
| 253 | /// three renderers already share for a different question -- whether a value is |
| 254 | /// complete the moment it changes. The sets nearly agree and the reasons do |
| 255 | /// not: a range is built up by dragging, so it is incomplete on the way and is |
| 256 | /// still not typed into. |
| 257 | /// |
| 258 | /// `false` for a kind this renderer has not met. [`layout::FieldKind`] is |
| 259 | /// `#[non_exhaustive]` and `input` is the wider event of the two -- it fires |
| 260 | /// for typing as well -- so an unknown kind is heard rather than missed. |
| 261 | const |
| 262 | matches! |
| 263 | kind, |
| 264 | Text |
| 265 | | Secret |
| 266 | | Number |
| 267 | | Interval |
| 268 | |
| 269 | | Url |
| 270 | | Tel |
| 271 | | Textarea |
| 272 | | Rich |
| 273 | |
| 274 | |
| 275 | |
| 276 | /// How a field's question is raised: on the keystroke, or on the value moving. |
| 277 | /// |
| 278 | /// [`Field::consults`] and [`Slot::consults`] are one idea at two scopes, and |
| 279 | /// only the region's heard a discrete control -- because a region's dials were |
| 280 | /// never all typed into and a field's were assumed to be. They are not: a |
| 281 | /// folder select that re-asks for a list of mail is the same question a search |
| 282 | /// box asks, and it is the majority shape rather than the exception. |
| 283 | /// |
| 284 | /// The event is this renderer's and the wait is the description's, which is why |
| 285 | /// only the first half is decided here. A discrete control's description says |
| 286 | /// [`Consult::after`] of zero, which is a real number for it rather than a |
| 287 | /// meaningless one -- [`Slot::consults`] already applies its wait to a radio |
| 288 | /// group deliberately. |
| 289 | /// |
| 290 | /// [`Field::consults`]: quasi_router::Field::consults |
| 291 | /// [`Slot::consults`]: quasi_router::Slot::consults |
| 292 | /// [`Consult::after`]: quasi_router::Consult::after |
| 293 | |
| 294 | let after = consult.after; |
| 295 | let at_least = consult.at_least; |
| 296 | if typed_into |
| 297 | Typing |
| 298 | else |
| 299 | Working |
| 300 | |
| 301 | |
| 302 | |
| 303 | /// One field, with whatever `1c4a66a4` and `14612ed8` added around it. |
| 304 | /// |
| 305 | /// The field's own markup is makeover-webview's, unchanged. A second field |
| 306 | /// emitter here is the divergence phase A existed to end, and it would be the |
| 307 | /// same anatomy with a different escaping story. |
| 308 | /// |
| 309 | /// A [`Field::writes`] is a wrapper rather than attributes on the control, |
| 310 | /// because the control is emitted by makeover-webview and there is no seam to |
| 311 | /// put them through. That turns out to be the better shape anyway: the `change` |
| 312 | /// event bubbles, so one element around the group catches it whichever of the |
| 313 | /// input, select or textarea forms the field took, and `hx-include` finds the |
| 314 | /// control back without this having to know which it was. |
| 315 | |
| 316 | // A question that only sometimes applies is drawn inside a box carrying the |
| 317 | // condition, so the reveal script settles it exactly as it settles a |
| 318 | // region, and the control stays inside the form and still submits. |
| 319 | // `8fdb814c`. |
| 320 | if let Some = &field.revealed_by |
| 321 | out.push_str; |
| 322 | class_attr; |
| 323 | reveal_attrs; |
| 324 | out.push; |
| 325 | let mut inner = clone; |
| 326 | inner.revealed_by = None; |
| 327 | field_group_html; |
| 328 | out.push_str; |
| 329 | return; |
| 330 | |
| 331 | |
| 332 | // A question answered N times is N slots and two controls, and each slot is |
| 333 | // an ordinary field of this same emitter. `60d1753c`. |
| 334 | if let Some = &field.repeats |
| 335 | repeat_html; |
| 336 | return; |
| 337 | |
| 338 | let mut filling = of; |
| 339 | let writes = field.writes.as_ref; |
| 340 | |
| 341 | // A field that owns a suggestion list is a combobox, and all three parts of |
| 342 | // one hang off the field's own name: the box's id is the name, the list's |
| 343 | // id is `suggestions_id` of it, and nothing is authored anywhere. That is |
| 344 | // what ownership buys over two elements pointed at each other by hand. |
| 345 | // |
| 346 | // Gated on the name being a handle, for `hyperscript::handle`'s reason: the |
| 347 | // programs below address both elements by id inside a selector inside an |
| 348 | // attribute. A name that is not a plain handle draws the field it drew |
| 349 | // before this existed, which is a fallback rather than a break. |
| 350 | let suggests = field |
| 351 | .suggests |
| 352 | .as_ref |
| 353 | .zip; |
| 354 | let wiring = suggests.map |
| 355 | let list = suggestions_id; |
| 356 | |
| 357 | ; |
| 358 | let listbox = wiring |
| 359 | .as_ref |
| 360 | .map; |
| 361 | if let Some = listbox.as_ref |
| 362 | filling.trailing = Some; |
| 363 | |
| 364 | |
| 365 | // Everything this renderer knows about the control element that no |
| 366 | // description layer carries, gathered before it is handed over: there is |
| 367 | // one `control_attrs` slot and two things that want it. `a135f898` is the |
| 368 | // second, and it arrived after the combobox had taken the slot. |
| 369 | let mut control_attrs = Stringnew; |
| 370 | if let Some = wiring.as_ref |
| 371 | control_attrs.push_str; |
| 372 | |
| 373 | // What is in this box is the reader's, so a swap that redraws it must not |
| 374 | // take it away. `hx-preserve` is htmx's word for exactly that, and it works |
| 375 | // off the `id` makeover already writes from the field's name. |
| 376 | // |
| 377 | // The one host where this has to be said: a terminal keeps its own buffer |
| 378 | // and egui keeps widget state by id, so both were already right, and a |
| 379 | // browser replaces the element and takes what was typed with it. |
| 380 | if field.keeps_value |
| 381 | if !control_attrs.is_empty |
| 382 | control_attrs.push; |
| 383 | |
| 384 | preserve_attr; |
| 385 | |
| 386 | // Where the caret starts, when the screen said so and this answer is a |
| 387 | // whole document. `Doc::opens_at` is `None` on every fragment, which is the |
| 388 | // whole of the guard: a swap that moves the caret takes it out of whatever |
| 389 | // the reader was typing into, and a browser answers most interactions with |
| 390 | // a fragment. |
| 391 | // |
| 392 | // Nothing counts the emissions. Two fields under one name is an app |
| 393 | // disagreeing with itself, and HTML already rules on it -- the first |
| 394 | // `autofocus` in tree order is the one a browser honours -- so a counter |
| 395 | // here would only be a second, quieter answer to a question the platform |
| 396 | // has settled. |
| 397 | if doc.opens |
| 398 | if !control_attrs.is_empty |
| 399 | control_attrs.push; |
| 400 | |
| 401 | control_attrs.push_str; |
| 402 | |
| 403 | if !control_attrs.is_empty |
| 404 | filling.control_attrs = Some; |
| 405 | |
| 406 | |
| 407 | // Outside the consult wrappers below, for their own reason one turn further |
| 408 | // out: htmx takes one verb and one address per element, and the question a |
| 409 | // field owns is a different question from the ones it merely asks. |
| 410 | if let Some = wiring.as_ref |
| 411 | out.push_str; |
| 412 | class_attr; |
| 413 | let sends = sends_selector; |
| 414 | // The answer lands in the list the field owns, always. An |
| 415 | // `Action::replacing` on a suggestion source is a description arguing |
| 416 | // with itself -- it says the candidates go somewhere other than the |
| 417 | // control they are candidates for -- so it is overwritten here rather |
| 418 | // than honoured, and the combobox wiring cannot come apart from the |
| 419 | // element it names. |
| 420 | let mut action = consult.action.clone; |
| 421 | action.replaces = Some; |
| 422 | action_attrs |
| 423 | &action, |
| 424 | asking, |
| 425 | None, |
| 426 | sends.as_deref, |
| 427 | false, |
| 428 | out, |
| 429 | ; |
| 430 | out.push; |
| 431 | |
| 432 | |
| 433 | // A consult is a second wrapper outside the first rather than more |
| 434 | // attributes on it. Two routes cannot share one element: htmx takes one |
| 435 | // verb and one address per element, and a field that both writes when it |
| 436 | // settles and asks about itself while it is being typed names two. Outside |
| 437 | // rather than inside so the width and the tick-gathering stay on the |
| 438 | // element that already carries them. |
| 439 | // |
| 440 | // One wrapper each where there are several. A box asking two routes at two |
| 441 | // rates is MNW's discover search, and the two questions cannot share an |
| 442 | // element for the reason a consult cannot share one with a write. |
| 443 | for consult in &field.consults |
| 444 | out.push_str; |
| 445 | class_attr; |
| 446 | // What rides along, named by field name and turned into a selector |
| 447 | // here. The names are the description's — they are what a submit sends |
| 448 | // each value under — and reducing them to a document selector is this |
| 449 | // renderer's job, which is the whole reason the vocabulary does not |
| 450 | // carry the `.discover-filter` class the shipped markup groups by. |
| 451 | let sends = sends_selector; |
| 452 | action_attrs |
| 453 | &consult.action, |
| 454 | asking, |
| 455 | None, |
| 456 | sends.as_deref, |
| 457 | false, |
| 458 | out, |
| 459 | ; |
| 460 | out.push; |
| 461 | |
| 462 | |
| 463 | // The width sits on the wrapper, because what `field_html_into` emits is |
| 464 | // makeover's and this is quasi's fact about it. A field that writes already |
| 465 | // has a wrapper for the same reason; one that only asks for room gets the |
| 466 | // same box with nothing else on it. |
| 467 | let asked = !matches!; |
| 468 | if writes.is_none && asked |
| 469 | out.push_str; |
| 470 | class_attr; |
| 471 | width_attr; |
| 472 | out.push; |
| 473 | |
| 474 | |
| 475 | if let Some = writes |
| 476 | out.push_str; |
| 477 | class_attr; |
| 478 | width_attr; |
| 479 | action_attrs; |
| 480 | out.push; |
| 481 | |
| 482 | |
| 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 = crateopen; |
| 487 | if marked.watching |
| 488 | let mut placed = Vecnew; |
| 489 | field.with_layout |
| 490 | field_html_placed; |
| 491 | ; |
| 492 | for block in &placed |
| 493 | marked.wrote; |
| 494 | |
| 495 | marked.close; |
| 496 | else |
| 497 | field.with_layout |
| 498 | field_html_into; |
| 499 | ; |
| 500 | |
| 501 | |
| 502 | if writes.is_some || asked |
| 503 | out.push_str; |
| 504 | |
| 505 | for _ in &field.consults |
| 506 | out.push_str; |
| 507 | |
| 508 | if wiring.is_some |
| 509 | out.push_str; |
| 510 | |
| 511 | |
| 512 | |
| 513 | /// A question answered zero or more times: the slots, and the controls that add |
| 514 | /// and remove one. |
| 515 | /// |
| 516 | /// A `fieldset` because that is what a group of controls answering one |
| 517 | /// question is in HTML, and the legend is the question. Each slot is |
| 518 | /// [`Field::instance`] put back through [`field_group_html`], so a slot is |
| 519 | /// drawn by everything this renderer already does to a field rather than by a |
| 520 | /// second emitter. |
| 521 | /// |
| 522 | /// # What the reader does here costs no round trip |
| 523 | /// |
| 524 | /// Adding a slot clones the blank one out of the `template` and removing one |
| 525 | /// takes the slot out of the document, both in `repeat.js`. A description that |
| 526 | /// had to ask a route for another empty box would re-render a form the reader |
| 527 | /// is midway through, which is `a135f898` and is the same objection that put |
| 528 | /// the reveal condition in a script. |
| 529 | /// |
| 530 | /// The hooks the script reads are `data-` attributes rather than classes, for |
| 531 | /// `data-reveal`'s reason: a class is prefixed by host configuration and a |
| 532 | /// script shipped by this crate cannot know the prefix. |
| 533 | /// |
| 534 | /// # A page served without the script |
| 535 | /// |
| 536 | /// Every slot the description offered is there, fillable and submittable, and |
| 537 | /// the two controls do nothing. That is the direction the rest of this renderer |
| 538 | /// degrades in: what the handler said, without what the reader could have added |
| 539 | /// to it. |
| 540 | |
| 541 | let standing = repeat.standing; |
| 542 | out.push_str; |
| 543 | class_attr; |
| 544 | out.push_str; |
| 545 | escape_into; |
| 546 | // The question's own label, so the script can renumber the slots it |
| 547 | // renames. `Repeat::ordinal` is what wrote them in the first place, and the |
| 548 | // two spellings are the same string in the same order. |
| 549 | out.push_str; |
| 550 | escape_into; |
| 551 | let _ = write!; |
| 552 | if let Some = repeat.add.from |
| 553 | out.push_str; |
| 554 | escape_into; |
| 555 | out.push; |
| 556 | |
| 557 | if let Some = repeat.most |
| 558 | let _ = write!; |
| 559 | |
| 560 | out.push; |
| 561 | |
| 562 | out.push_str; |
| 563 | class_attr; |
| 564 | out.push; |
| 565 | escape_into; |
| 566 | out.push_str; |
| 567 | |
| 568 | // What is wrong with the *set*, which is the fact a per-slot message cannot |
| 569 | // carry: "at most eight reminders" is about none of them in particular. A |
| 570 | // slot's own error rides on the slot, through `Field::error` as every other |
| 571 | // field's does. |
| 572 | if let Some = &field.error |
| 573 | out.push_str; |
| 574 | class_attr; |
| 575 | out.push_str; |
| 576 | escape_into; |
| 577 | out.push_str; |
| 578 | |
| 579 | |
| 580 | out.push_str; |
| 581 | class_attr; |
| 582 | // The hook the script appends into. A container of its own rather than the |
| 583 | // fieldset, so a slot cloned into a group with none standing lands above |
| 584 | // the add control rather than under it. |
| 585 | out.push_str; |
| 586 | for at in 0..standing |
| 587 | repeat_slot_html; |
| 588 | |
| 589 | out.push_str; |
| 590 | |
| 591 | // The blank the add control clones. Inert markup until a script takes it |
| 592 | // out, which is what a `template` is for, and it carries the next index so |
| 593 | // that pressing add once with no script running is the only thing that ever |
| 594 | // needed renumbering. |
| 595 | out.push_str; |
| 596 | class_attr; |
| 597 | out.push; |
| 598 | // The question itself, at an index past the last standing slot. `instance` |
| 599 | // and `instance_part` both answer blank there, which is what the template |
| 600 | // needs, and a grouped question keeps its parts this way where a cleared |
| 601 | // `repeats` would have left the template with no questions to draw. |
| 602 | repeat_slot_html; |
| 603 | out.push_str; |
| 604 | |
| 605 | // A question whose slots come from another control offers no add control |
| 606 | // of its own: there is no blank a reader could fill. The script is told |
| 607 | // which control makes them, on the group, so it needs no second address. |
| 608 | if let Some = repeat.add.label |
| 609 | out.push_str; |
| 610 | class_attr; |
| 611 | out.push_str; |
| 612 | if !repeat.more |
| 613 | out.push_str; |
| 614 | |
| 615 | out.push; |
| 616 | escape_into; |
| 617 | out.push_str; |
| 618 | |
| 619 | out.push_str; |
| 620 | |
| 621 | |
| 622 | /// One slot: the question under its indexed name, and the control that takes it |
| 623 | /// away. |
| 624 | /// |
| 625 | /// `standing` is how many slots there are, which is what decides whether this |
| 626 | /// one may be removed. The script recomputes the same answer after every change |
| 627 | /// from `data-repeat-least`, so the first paint and every one after it agree. |
| 628 | |
| 629 | field: &Field, |
| 630 | repeat: &Repeat, |
| 631 | at: usize, |
| 632 | standing: usize, |
| 633 | opts: &Emit, |
| 634 | doc: &, |
| 635 | out: &mut String, |
| 636 | |
| 637 | let slot = repeat.instances.get; |
| 638 | let progress = slot.map_or; |
| 639 | out.push_str; |
| 640 | class_attr; |
| 641 | let _ = write!; |
| 642 | // The slot is named by what is in it rather than by where it is, so the |
| 643 | // script must not rewrite the ordinal over the top of it. |
| 644 | if slot.is_some_and |
| 645 | out.push_str; |
| 646 | |
| 647 | // Stated rather than inferred from the markup inside, so the script and the |
| 648 | // stylesheet read the slot's state from one place and a slot mid-flight is |
| 649 | // the same string on every host. |
| 650 | match progress |
| 651 | Idle => |
| 652 | Working => out.push_str, |
| 653 | Done => out.push_str, |
| 654 | Failed => out.push_str, |
| 655 | |
| 656 | out.push; |
| 657 | for slot in field.instance_fields |
| 658 | field_group_html; |
| 659 | |
| 660 | // What is wrong with the slot as a whole, which a part's own error cannot |
| 661 | // carry: the upload that failed, not the name that was too long. |
| 662 | if let Some = slot.and_then |
| 663 | out.push_str; |
| 664 | class_attr; |
| 665 | out.push_str; |
| 666 | escape_into; |
| 667 | out.push_str; |
| 668 | |
| 669 | // makeover's own meter, not a second spelling of one: how far a slot has |
| 670 | // got is the proportion this renderer already knows how to draw. |
| 671 | if let Working = progress |
| 672 | meter_html_into; |
| 673 | |
| 674 | out.push_str; |
| 675 | class_attr; |
| 676 | out.push_str; |
| 677 | // A slot the work has not finished with is one the reader may not pull out |
| 678 | // from under it, which is the floor's reason one state further along. |
| 679 | if !repeat.fewer || progress.busy |
| 680 | out.push_str; |
| 681 | |
| 682 | out.push; |
| 683 | escape_into; |
| 684 | out.push_str; |
| 685 | |
| 686 | |
| 687 | /// The document id of the list a field owns, from the field's own name. |
| 688 | /// |
| 689 | /// Derived and never authored, which is the difference between a list a field |
| 690 | /// owns and two elements a description has to keep pointing at each other. The |
| 691 | /// box's own id is the name, so the two cannot collide. |
| 692 | /// |
| 693 | /// The name is a [`handle`](crate::hyperscript::handle) at every call site, so |
| 694 | /// the result is one too and can be addressed from a program. |
| 695 | pub |
| 696 | format! |
| 697 | |
| 698 | |
| 699 | /// The id of the popover container belonging to an anchorable thing. |
| 700 | /// |
| 701 | /// [`suggestions_id`]'s sibling and derived the same way: a container the |
| 702 | /// description never names, minted from the name the description does carry, |
| 703 | /// so nothing has to point two elements at each other. |
| 704 | /// |
| 705 | /// The handle is a region's [`Slot::id`](quasi_router::Slot::id) or a control's |
| 706 | /// [`Act::id`](quasi_router::Act::id). A screen's selection has neither and |
| 707 | /// needs neither -- see [`SELECTION_ANCHOR_ID`]. |
| 708 | pub |
| 709 | format! |
| 710 | |
| 711 | |
| 712 | /// The id of the popover container for the screen's selection. |
| 713 | /// |
| 714 | /// Fixed rather than derived, because a screen holds one selection |
| 715 | /// ([`Screen::selection`](quasi_router::Screen::selection)) and |
| 716 | /// [`Anchor::Selection`](quasi_router::Anchor::Selection) therefore carries no |
| 717 | /// name to derive one from. It becomes derived on the day that field becomes a |
| 718 | /// map, which is the same day [`Act::over`](quasi_router::Act::over) starts |
| 719 | /// choosing between sets. |
| 720 | pub const SELECTION_ANCHOR_ID: &str = "quasi-selection-anchored"; |
| 721 | |
| 722 | /// An empty container for an anchored screen to land in. |
| 723 | /// |
| 724 | /// Emitted beside the thing it belongs to and left empty until a route answers |
| 725 | /// with an [`Outcome::Anchored`](quasi_router::Outcome::Anchored), which is |
| 726 | /// exactly how a row's menu is emitted: a menu is a container the host opens its |
| 727 | /// own way, and this is that container for the things that are not rows. |
| 728 | /// |
| 729 | /// `hidden` for the same reason a row's menu carries it. An empty container is |
| 730 | /// invisible either way; saying so is what keeps it out of the accessible tree |
| 731 | /// until there is something in it. |
| 732 | pub |
| 733 | out.push_str; |
| 734 | class_attr; |
| 735 | out.push_str; |
| 736 | escape_into; |
| 737 | // Named rather than found by class, so a host's own menu code binds to |
| 738 | // something a `class_prefix` cannot move. `data-menu="anchored"` and not |
| 739 | // `"row"`: both are menus and only one of them belongs to a row. |
| 740 | out.push_str; |
| 741 | |
| 742 | |
| 743 | /// The combobox attributes that go on the control itself. |
| 744 | /// |
| 745 | /// Through `makeover-webview`'s `Filling::control_attrs`, which exists for this: |
| 746 | /// what makes a box a combobox is the list it owns, and no description layer |
| 747 | /// carries one. `aria-expanded` starts false because a field is drawn before it |
| 748 | /// has been typed into, and the list's own program is what moves it. |
| 749 | /// |
| 750 | /// `autocomplete="off"` because the browser's own list would sit on top of this |
| 751 | /// one, offering what it remembers being typed here instead of what the route |
| 752 | /// just answered. |
| 753 | |
| 754 | let mut attrs = format! |
| 755 | "role=\"combobox\" aria-autocomplete=\"list\" aria-expanded=\"false\" \ |
| 756 | aria-controls=\"{list}\" autocomplete=\"off\"" |
| 757 | ; |
| 758 | cratecombobox; |
| 759 | attrs |
| 760 | |
| 761 | |
| 762 | /// The list a field owns, empty until the route answers. |
| 763 | /// |
| 764 | /// Emitted with the field rather than by the answer, so the box has something |
| 765 | /// to point `aria-controls` at from the first draw. A reader told about a list |
| 766 | /// that is not in the document yet is told about nothing. |
| 767 | |
| 768 | let mut out = Stringnew; |
| 769 | out.push_str; |
| 770 | class_attr; |
| 771 | out.push_str; |
| 772 | escape_into; |
| 773 | // Named by the field it belongs to. A listbox with no accessible name is |
| 774 | // announced as a list of nothing in particular, and the box's own label is |
| 775 | // what a reader has just heard. |
| 776 | out.push_str; |
| 777 | escape_into; |
| 778 | out.push; |
| 779 | cratesuggestion_list; |
| 780 | out.push_str; |
| 781 | out |
| 782 | |
| 783 | |
| 784 | /// One candidate, as the answer to the question a field owns. |
| 785 | /// |
| 786 | /// [`Outcome::Suggestions`](quasi_router::Outcome::Suggestions) rendered: the |
| 787 | /// inside of the list, and nothing else. What each entry carries is the pair |
| 788 | /// [`Choice`] carries — the value is what the box is set to and the label is |
| 789 | /// what is read — plus its index, which is how the keyboard finds the next one |
| 790 | /// without counting the ones before it. |
| 791 | /// |
| 792 | /// A candidate that cannot be picked is announced and not offered: |
| 793 | /// `aria-disabled` says so, the reason is drawn beside it, and it carries |
| 794 | /// neither a value nor a program, so neither Enter nor a click can take it. |
| 795 | pub |
| 796 | let Some = cratehandle else |
| 797 | // The field this answers drew no list, for the same gate's reason, so |
| 798 | // there is nothing here to put candidates into. |
| 799 | return Stringnew; |
| 800 | ; |
| 801 | let list = suggestions_id; |
| 802 | let mut out = Stringnew; |
| 803 | for in options.iter.enumerate |
| 804 | // A row whose pick replaces the whole document is an anchor, because an |
| 805 | // anchor is what a browser navigates. `action_attrs` writes the `href` |
| 806 | // and no transport for such a pick (`00ee7af5`), and an `href` on a |
| 807 | // `div` is an attribute nothing reads: the row would close the list and |
| 808 | // stay where it was. Every other row is a `div`, since what picking it |
| 809 | // does is htmx's or the box's and neither wants a link. |
| 810 | let = match &choice.picks |
| 811 | Some if action.navigates => , |
| 812 | _ => , |
| 813 | ; |
| 814 | out.push_str; |
| 815 | class_attr; |
| 816 | // `aria-selected="false"` on every entry from the first draw, so the |
| 817 | // keyboard's program moves a value that is already there rather than |
| 818 | // introducing an attribute halfway through a list. |
| 819 | let _ = write! |
| 820 | out, |
| 821 | " id=\"{list}-{at}\" data-at=\"{at}\" role=\"option\" aria-selected=\"false\"" |
| 822 | ; |
| 823 | // What picking does. `ed1fa86f`: local by default, so a candidate that |
| 824 | // says nothing writes its value into the box exactly as before. One |
| 825 | // that carries an action has htmx call it, and nothing is written -- |
| 826 | // the search box's case, where a pick navigates and the typed value is |
| 827 | // discarded. |
| 828 | match &choice.picks |
| 829 | Some => |
| 830 | action_attrs; |
| 831 | cratesuggestion_acting; |
| 832 | |
| 833 | None => |
| 834 | out.push_str; |
| 835 | escape_into; |
| 836 | out.push; |
| 837 | cratesuggestion; |
| 838 | |
| 839 | |
| 840 | out.push; |
| 841 | escape_into; |
| 842 | // The second line, and the whole of why a candidate is not a `Choice` |
| 843 | // (`1fcf2e9b`). Its own element rather than folded into the label: it |
| 844 | // is styled apart, and it is not part of what the typed value matches. |
| 845 | if let Some = &choice.detail |
| 846 | out.push_str; |
| 847 | class_attr; |
| 848 | out.push; |
| 849 | escape_into; |
| 850 | out.push_str; |
| 851 | |
| 852 | out.push_str; |
| 853 | |
| 854 | out |
| 855 | |
| 856 | |
| 857 | /// The controls a [`Consult::sends`] names, as one selector. |
| 858 | /// |
| 859 | /// By `name`, which is what the description carries and what the route reads |
| 860 | /// each value back under. A field named here that is not on the screen |
| 861 | /// contributes a selector that matches nothing, which is the miss handled the |
| 862 | /// way every other address miss is: visible, and not worth refusing to draw a |
| 863 | /// screen over. |
| 864 | /// |
| 865 | /// `None` for the common field, which sends its own value and nothing else. |
| 866 | /// |
| 867 | /// [`Consult::sends`]: quasi_router::Consult::sends |
| 868 | |
| 869 | if names.is_empty |
| 870 | return None; |
| 871 | |
| 872 | let mut out = Stringnew; |
| 873 | for name in names |
| 874 | if !out.is_empty |
| 875 | out.push_str; |
| 876 | |
| 877 | out.push_str; |
| 878 | // The selector is written into an attribute, so the quoting has to |
| 879 | // survive both. A name carrying an apostrophe would otherwise end the |
| 880 | // selector's string early and change what is gathered. |
| 881 | out.push_str; |
| 882 | out.push_str; |
| 883 | |
| 884 | Some |
| 885 | |
| 886 | |
| 887 | /// Markdown source into markup, for [`Node::Rich`], composed from the node's |
| 888 | /// two axes rather than picked from a preset. |
| 889 | /// |
| 890 | /// docengine's presets are named points on a diagonal: `strict` turns tables, |
| 891 | /// footnotes and task lists **off** at the same time as it turns `nofollow`, |
| 892 | /// raw-HTML stripping and scheme filtering **on**. Those are two questions, and |
| 893 | /// reading them off one name is what made a page nofollow its own links -- the |
| 894 | /// only way to have prose with a followed link in it was to ask for the |
| 895 | /// permissive treatment of raw HTML as well. |
| 896 | /// |
| 897 | /// So this composes `docengine::Renderer` from [`Richness`] and [`Trust`] |
| 898 | /// independently. The four cells all mean something: |
| 899 | /// |
| 900 | /// ```text |
| 901 | /// Sentence + Untrusted a forum post (was `strict`) |
| 902 | /// Sentence + Trusted a page's own sentence (new, and the fix) |
| 903 | /// Document + Untrusted a creator's long description (no preset had this) |
| 904 | /// Document + Trusted the app's own long-form (was `permissive`) |
| 905 | /// ``` |
| 906 | /// |
| 907 | /// **`Sentence + Untrusted` is `Renderer::strict()` field for field**, deliberately: |
| 908 | /// it is what `Node::rich` meant before the axes existed, and every call site |
| 909 | /// written against the old member keeps exactly what it had. |
| 910 | /// |
| 911 | /// Unconditional. This sat behind a default-on `rich` feature until markdown was |
| 912 | /// made standard, and turning the feature off did not remove a cost so much as |
| 913 | /// produce a renderer that draws `**bold**` at the user. Markdown is what these |
| 914 | /// descriptions are made of, so rendering it is part of being a renderer rather |
| 915 | /// than an extra somebody opts into. |
| 916 | |
| 917 | let full = matches!; |
| 918 | let untrusted = matches!; |
| 919 | |
| 920 | permissive |
| 921 | // Richness: what the format may express. |
| 922 | .with_tables |
| 923 | .with_footnotes |
| 924 | .with_tasklists |
| 925 | .with_strikethrough |
| 926 | .with_smart_punctuation |
| 927 | .with_strip_images |
| 928 | // Strictness: what the app is willing to take from this author. Raw |
| 929 | // markup and fetchable schemes are the author's reach into the |
| 930 | // document, so both turn on exactly when the author is a stranger. |
| 931 | .with_strip_raw_html |
| 932 | .with_dangerous_scheme_filter |
| 933 | .with_sanitize |
| 934 | Strict |
| 935 | else |
| 936 | Permissive |
| 937 | |
| 938 | .render |
| 939 | |
| 940 | |
| 941 | /// JSON-encode a string into an attribute value, both rules in one pass. |
| 942 | /// |
| 943 | /// Small enough to own. Pulling in a JSON crate to write object literals of |
| 944 | /// strings would be the larger decision, and the encoder a renderer needs is |
| 945 | /// this: the six characters JSON requires escaped, plus a `\u00XX` form for the |
| 946 | /// rest of the C0 range. |
| 947 | /// |
| 948 | /// The HTML escaping is applied here rather than by the caller, which is what |
| 949 | /// lets the payload go straight into `out`. Building the object and then |
| 950 | /// escaping the whole of it allocated a `String` for each, per action, and |
| 951 | /// there was nothing in between to look at. |
| 952 | /// |
| 953 | /// The two rules compose in this order and only this order. JSON runs first, so |
| 954 | /// a quote in the text becomes `\"` and then `\"`; the backslash JSON adds |
| 955 | /// is not a character HTML encodes, and the quote HTML encodes is not one JSON |
| 956 | /// would look at twice. The structural quotes the object needs are written as |
| 957 | /// `"` directly, because they are markup rather than content. |
| 958 | |
| 959 | out.push_str; |
| 960 | for ch in text.chars |
| 961 | match ch |
| 962 | // JSON first, then the HTML form of what it produced. |
| 963 | '"' => out.push_str, |
| 964 | '\\' => out.push_str, |
| 965 | '\n' => out.push_str, |
| 966 | '\r' => out.push_str, |
| 967 | '\t' => out.push_str, |
| 968 | c if < 0x20 => |
| 969 | let _ = write!; |
| 970 | |
| 971 | // The rest of what an attribute value cannot carry. JSON has no |
| 972 | // opinion on any of these, so this arm is HTML's alone and matches |
| 973 | // `escape_into` character for character. |
| 974 | '&' => out.push_str, |
| 975 | '<' => out.push_str, |
| 976 | '>' => out.push_str, |
| 977 | '\'' => out.push_str, |
| 978 | c => out.push, |
| 979 | |
| 980 | |
| 981 | out.push_str; |
| 982 | |
| 983 | |
| 984 | /// The params as an `hx-vals` object, written into the attribute they land in. |
| 985 | |
| 986 | json_pairs_attr; |
| 987 | |
| 988 | |
| 989 | /// Values as a JSON array inside an attribute value. |
| 990 | /// |
| 991 | /// The values a region watches for, which are app text: a licence key, a |
| 992 | /// timezone kind, whatever a select offers. Same escaper as the object form, so |
| 993 | /// there is one place where JSON and HTML compose and not two. |
| 994 | /// The three attributes a conditional thing carries: which control it watches, |
| 995 | /// what that control has to hold, and the values that satisfy it. |
| 996 | /// |
| 997 | /// Written from one place for a region and for a single question inside a |
| 998 | /// form, because `reveal.js` reads the marks rather than the element: a `div` |
| 999 | /// and a field group answer the same script, and two emitters writing the same |
| 1000 | /// three names is how they come apart. |
| 1001 | |
| 1002 | out.push_str; |
| 1003 | escape_into; |
| 1004 | out.push; |
| 1005 | match &reveal.when |
| 1006 | Anything => out.push_str, |
| 1007 | Nothing => out.push_str, |
| 1008 | Value => |
| 1009 | out.push_str; |
| 1010 | json_values_attr; |
| 1011 | out.push; |
| 1012 | |
| 1013 | OneOf => |
| 1014 | out.push_str; |
| 1015 | json_values_attr; |
| 1016 | out.push; |
| 1017 | |
| 1018 | |
| 1019 | |
| 1020 | |
| 1021 | |
| 1022 | out.push; |
| 1023 | for in values.iter.enumerate |
| 1024 | if i > 0 |
| 1025 | out.push; |
| 1026 | |
| 1027 | json_string_attr; |
| 1028 | |
| 1029 | out.push; |
| 1030 | |
| 1031 | |
| 1032 | /// Name/value pairs as a JSON object inside an attribute value. |
| 1033 | /// |
| 1034 | /// Shared with the shell, which writes the headers a document sends into |
| 1035 | /// `hx-headers`. One escaper for both, since the two attributes have the same |
| 1036 | /// shape and a second implementation is a second thing to get wrong. |
| 1037 | pub |
| 1038 | |
| 1039 | I: , |
| 1040 | |
| 1041 | out.push; |
| 1042 | for in pairs.into_iter.enumerate |
| 1043 | if i > 0 |
| 1044 | out.push; |
| 1045 | |
| 1046 | json_string_attr; |
| 1047 | out.push; |
| 1048 | json_string_attr; |
| 1049 | |
| 1050 | out.push; |
| 1051 | |
| 1052 | |
| 1053 | /// What makes a control call its action. |
| 1054 | /// |
| 1055 | /// Here rather than at the call sites because every `hx-` attribute this crate |
| 1056 | /// emits has to come out of one function, which is decision 13's claim that the |
| 1057 | /// transport is replaceable and is asserted by a test. |
| 1058 | |
| 1059 | pub |
| 1060 | /// The user activating the control. htmx's default for a button or a link. |
| 1061 | Click, |
| 1062 | /// The user pressing a tab whose panel is a route. |
| 1063 | /// |
| 1064 | /// A gesture like [`Click`](Self::Click), and the second member that |
| 1065 | /// suppresses the `href` below, for a different reason than |
| 1066 | /// [`Load`](Self::Load): a tab strip's control *is* somewhere to go, and the |
| 1067 | /// place it goes is a fragment. An `href` to one is a link that middle-click, |
| 1068 | /// copy-link and a crawler would all follow to a shell-less scrap of a page, |
| 1069 | /// which is exactly the lying control `control_tag` exists to prevent -- |
| 1070 | /// pointing the other way. So the strip keeps the `<button>` it has always |
| 1071 | /// emitted and the address stays on `hx-get` alone. |
| 1072 | ClickInStrip, |
| 1073 | /// The form being submitted. htmx's own default for a `<form>`, so no |
| 1074 | /// `hx-trigger` is emitted for it either. |
| 1075 | /// |
| 1076 | /// Distinguished from [`Click`](Self::Click) for one reason, and it is |
| 1077 | /// [`guards`](Self::guards): a waiting form cannot lock itself. `disabled` |
| 1078 | /// is not an attribute a `<form>` has and it does not reach descendants the |
| 1079 | /// way `<fieldset disabled>` does, so the guard has to name the button. |
| 1080 | Submit, |
| 1081 | /// The element existing. What a region fed by a call uses, so the screen |
| 1082 | /// paints its stand-in and the slow part arrives behind it. |
| 1083 | /// |
| 1084 | /// The only member that is not a gesture, which is why it is the only one |
| 1085 | /// that suppresses the `href` below: a region is not somewhere to go. |
| 1086 | Load, |
| 1087 | /// The element existing, and then every [`CADENCE`](crate::CADENCE) |
| 1088 | /// afterwards. |
| 1089 | /// |
| 1090 | /// [`Slot::live`](quasi_router::Slot::live) on a region that also names a |
| 1091 | /// call. `load` is kept beside the interval rather than replaced by it, |
| 1092 | /// because htmx's `every` waits out the first period before its first |
| 1093 | /// request and a region that painted its stand-in and then sat empty for |
| 1094 | /// ten seconds is a slower screen than the one this replaces. |
| 1095 | /// |
| 1096 | /// Not a gesture either, and suppresses the `href` for |
| 1097 | /// [`Load`](Self::Load)'s reason. |
| 1098 | Live, |
| 1099 | /// The control's own value changing. What a checkbox that is itself the |
| 1100 | /// write does. |
| 1101 | Change, |
| 1102 | /// The value of a control *inside* this element changing. |
| 1103 | /// |
| 1104 | /// A field group, whose control is emitted by makeover-webview and has no |
| 1105 | /// seam to hang attributes on. The `change` event bubbles, so the wrapper |
| 1106 | /// catches it whichever of input, select or textarea the field turned out to |
| 1107 | /// be, and the value is found back rather than assumed. |
| 1108 | ChangeInside, |
| 1109 | /// A key pressed anywhere in the document. |
| 1110 | /// |
| 1111 | /// What a [`Chrome`](quasi_router::Chrome) binding is: it belongs to no |
| 1112 | /// element, so it listens on the body rather than on itself. The string is |
| 1113 | /// the filter over `KeyboardEvent`, built by `crate::chrome` because |
| 1114 | /// reading a key name is the host's job. |
| 1115 | Key, |
| 1116 | /// The user clicking the element, but not a control inside it. |
| 1117 | /// |
| 1118 | /// A table row carries its `activate` on the row itself, unlike a list |
| 1119 | /// row, which hangs it on the primary text and so has never had this |
| 1120 | /// problem. Once a cell can hold a control, a click on that control |
| 1121 | /// bubbles to the row and htmx fires both: pressing Remove would delete |
| 1122 | /// the key and open it. The filter is on the row rather than a |
| 1123 | /// `stopPropagation` on the button because the row is the element making |
| 1124 | /// the wrong assumption, and a button that swallows events breaks anything |
| 1125 | /// else listening above it. |
| 1126 | ClickBeside, |
| 1127 | /// The value of a control inside this element being typed into, once it has |
| 1128 | /// stood still for the given wait. |
| 1129 | /// |
| 1130 | /// A [`Field::consults`] on a control the reader types into, which is what |
| 1131 | /// [`typed_into`] decides. `keyup changed` rather than `change`, because |
| 1132 | /// the question is about the value being written and `change` does not fire |
| 1133 | /// until the box is left. The delay is the description's, not this |
| 1134 | /// renderer's: the four sites this replaces each spelled `delay:500ms` by |
| 1135 | /// hand, and a number picked here is a number the terminal renderer would |
| 1136 | /// have had to guess at separately. |
| 1137 | /// |
| 1138 | /// Every other control on a field's consult takes |
| 1139 | /// [`Working`](Self::Working) instead, `aeb44860`: `keyup` never hears a |
| 1140 | /// select move. |
| 1141 | /// |
| 1142 | /// [`Field::consults`]: quasi_router::Field::consults |
| 1143 | Typing |
| 1144 | /// How long the value stands still first. |
| 1145 | after: Duration, |
| 1146 | /// How many characters it must carry before the question is asked at |
| 1147 | /// all. `0` asks whatever is there. |
| 1148 | at_least: usize, |
| 1149 | , |
| 1150 | /// Any control inside this element moving, once it has stood still for the |
| 1151 | /// given wait. |
| 1152 | /// |
| 1153 | /// [`Slot::consults`], and a [`Field::consults`] on anything |
| 1154 | /// [`typed_into`] refuses. The `input` event and not |
| 1155 | /// [`Typing`](Self::Typing)'s |
| 1156 | /// `keyup`, because these dials are not all typed into: MNW's fee |
| 1157 | /// calculator has four number boxes and a radio group, and `keyup` never |
| 1158 | /// hears the radio. `input` is what the shipped markup already reaches for |
| 1159 | /// on the four boxes, it fires for a radio, a checkbox and a select as |
| 1160 | /// well, and it does not fire on blur -- which `change` does, so the pair |
| 1161 | /// `keyup, change` would ask the same question twice for every box the |
| 1162 | /// reader typed in and then left. |
| 1163 | /// |
| 1164 | /// The wait applies to the radio as much as to the boxes. That is a |
| 1165 | /// difference from the hand-written markup, which delays the boxes and |
| 1166 | /// fires the radio at once, and it is the description's number rather than |
| 1167 | /// this renderer's: a description that wanted the two to differ would say |
| 1168 | /// so with two consults. A field's discrete consult says zero and means it. |
| 1169 | /// |
| 1170 | /// [`Field::consults`]: quasi_router::Field::consults |
| 1171 | /// [`Slot::consults`]: quasi_router::Slot::consults |
| 1172 | Working |
| 1173 | /// How long the value that moved stands still first. |
| 1174 | after: Duration, |
| 1175 | /// How many characters that value must carry before the question is |
| 1176 | /// asked at all. `0` asks whatever is there. |
| 1177 | at_least: usize, |
| 1178 | , |
| 1179 | |
| 1180 | |
| 1181 | |
| 1182 | /// The key filter, when the gesture is a keystroke. |
| 1183 | /// |
| 1184 | /// One caller: the destination that goes back, which is the one this |
| 1185 | /// renderer performs itself rather than handing to htmx. Every other |
| 1186 | /// destination gets its gesture written as an `hx-trigger` and never has to |
| 1187 | /// ask what the gesture was. |
| 1188 | const |
| 1189 | match self |
| 1190 | SelfKey => Some, |
| 1191 | _ => None, |
| 1192 | |
| 1193 | |
| 1194 | |
| 1195 | /// The selector this trigger needs gathered, beyond anything the caller |
| 1196 | /// asked for. |
| 1197 | /// |
| 1198 | /// Only one kind has one: a field group's control is inside it rather than |
| 1199 | /// being it, so the value has to be found back. Returned rather than |
| 1200 | /// written so it can be joined with the screen's ticks — two `hx-include` |
| 1201 | /// attributes on one element is markup a parser drops half of. |
| 1202 | const |
| 1203 | match self |
| 1204 | SelfChangeInside | SelfTyping | SelfWorking => |
| 1205 | Some |
| 1206 | |
| 1207 | SelfClick |
| 1208 | | SelfClickInStrip |
| 1209 | | SelfSubmit |
| 1210 | | SelfChange |
| 1211 | | SelfKey |
| 1212 | | SelfClickBeside |
| 1213 | | SelfLoad |
| 1214 | | SelfLive => None, |
| 1215 | |
| 1216 | |
| 1217 | |
| 1218 | /// What an [`Action::awaiting`] on this trigger locks while it waits. |
| 1219 | /// |
| 1220 | /// Everything that fires on a gesture is the element the gesture landed |
| 1221 | /// on, so `this` locks the thing that was pressed. A form is the exception |
| 1222 | /// and the reason this exists: htmx sets `disabled` on whatever the |
| 1223 | /// selector resolves to, a `<form>` has no such attribute, and it does not |
| 1224 | /// cascade. `hx-disable="this"` on a form is therefore a double-submit |
| 1225 | /// guard that permits a double submit — worse than none, because a |
| 1226 | /// consumer deletes a working hand-rolled guard to adopt it. |
| 1227 | /// |
| 1228 | /// The button rather than every control inside: it is what has to stop |
| 1229 | /// answering, and locking the fields as well would take the text away from |
| 1230 | /// a reader still correcting it. |
| 1231 | const |
| 1232 | match self |
| 1233 | SelfSubmit => "find button[type='submit']", |
| 1234 | SelfClick |
| 1235 | | SelfClickInStrip |
| 1236 | | SelfChange |
| 1237 | | SelfKey |
| 1238 | | SelfClickBeside |
| 1239 | | SelfLoad |
| 1240 | | SelfLive |
| 1241 | | SelfChangeInside |
| 1242 | | SelfTyping |
| 1243 | | SelfWorking => "this", |
| 1244 | |
| 1245 | |
| 1246 | |
| 1247 | |
| 1248 | /// The transport attributes for one action. |
| 1249 | /// |
| 1250 | /// # The two bags land in two places, and that is the point |
| 1251 | /// |
| 1252 | /// An action carries what the control sends (`params`) and the view it was |
| 1253 | /// offered under (`carried`), and htmx has a slot for each: the address takes |
| 1254 | /// the view, `hx-vals` takes the payload. So a write to a filtered list emits |
| 1255 | /// `hx-post="/problems/{id}/status?status=Open"` with |
| 1256 | /// `hx-vals='{"status":"Dismissed"}'`, and the two `status` values never meet. |
| 1257 | /// |
| 1258 | /// `hx-vals` is a JSON object literal, so two entries under one name emitted a |
| 1259 | /// duplicate key and every parser kept the last — inverting |
| 1260 | /// [`Params::get`]'s first-wins rule the moment a value crossed the wire, and |
| 1261 | /// silently dropping every repeat that [`Params::get_all`] exists to carry. |
| 1262 | /// Folding the view into the address fixes both: a query string repeats a name |
| 1263 | /// happily, and it is the half that wanted to be in the URL anyway. |
| 1264 | /// |
| 1265 | /// Nothing here concatenates a `?`. [`quasi_http::route_url`] does that, in one |
| 1266 | /// place, with a real encoder, because hand-built query strings are where |
| 1267 | /// escaping bugs live. |
| 1268 | /// |
| 1269 | /// The separation is a write's. On a read htmx puts `hx-vals` in the query |
| 1270 | /// string too, so the two bags land in one place either way, and htmx 4 deletes |
| 1271 | /// an address parameter whose name a value repeats before appending it, where |
| 1272 | /// 2.x appended both. So a GET naming one thing in both bags sends the payload |
| 1273 | /// and not the view. Nothing in the tree does; a read that wanted both would |
| 1274 | /// have to say which one it means, and this is where to look when it does. |
| 1275 | pub |
| 1276 | action: &Action, |
| 1277 | fires: Fires, |
| 1278 | confirm: , |
| 1279 | gathers: , |
| 1280 | chooses: bool, |
| 1281 | out: &mut String, |
| 1282 | |
| 1283 | // Neither way out of the app is htmx's business: nothing swaps, no route is |
| 1284 | // called, and the browser follows a normal link. |
| 1285 | // |
| 1286 | // The two differ in where the reader ends up, which is the description's |
| 1287 | // (`48a6e9e5`). `External` is a reference to come back from, so it is put |
| 1288 | // aside in a tab; `Leaving` is this document handing the reader on, so it |
| 1289 | // replaces the page, which is what a bare `href` already does. |
| 1290 | // |
| 1291 | // `rel` on both rather than only on the tab. It is `noopener` that needs |
| 1292 | // the argument and it needs it once: a new tab with `window.opener` intact |
| 1293 | // hands the other page a handle on this one, and while a same-tab |
| 1294 | // navigation gives it no live handle, it still sends `Referer`, which |
| 1295 | // `noreferrer` is what stops. Neither is a cost, and a branch here would be |
| 1296 | // a second thing to get right. |
| 1297 | if let External | Leaving = &action.destination |
| 1298 | out.push_str; |
| 1299 | escape_into; |
| 1300 | out.push; |
| 1301 | if matches! |
| 1302 | out.push_str; |
| 1303 | |
| 1304 | out.push_str; |
| 1305 | return; |
| 1306 | |
| 1307 | |
| 1308 | // Nothing leaves the machine, so nothing htmx does applies: no verb, no |
| 1309 | // trigger, no `href`, and above all no address -- a local destination has |
| 1310 | // none, and `route_url` below would build `hx-get=""`, which htmx reads as |
| 1311 | // "ask the page you are on". That is the failure this branch exists to make |
| 1312 | // impossible, and it is why the branch is here rather than left to the |
| 1313 | // `matches!` further down. |
| 1314 | // |
| 1315 | // `210574ca`, ruled 2026-08-18. This crate is the one |
| 1316 | // `Renderer::Hybrid` renderer, so it is the one that has to be told. |
| 1317 | // |
| 1318 | // What it emits is the mark and not the behaviour, and that is unchanged by |
| 1319 | // the hyperscript pass of `0081563e`: what happens locally is named by the |
| 1320 | // member carrying the action, and no member names one yet, so there is no |
| 1321 | // program to write here. `crate::hyperscript` is where it goes when there |
| 1322 | // is, and the attribute is what makes every site findable meanwhile. |
| 1323 | // Wherever the reader came from. The browser is the host holding that |
| 1324 | // history, so the behaviour is emitted rather than marked: this is the one |
| 1325 | // destination a webview can perform on its own, and `crate::hyperscript` |
| 1326 | // carries why an attribute is allowed to hold the program. |
| 1327 | // |
| 1328 | // Before the `Local` branch and not folded into it: `is_local` is false |
| 1329 | // here, and the mark that branch writes says "no request at all", which is |
| 1330 | // not what going back is. |
| 1331 | if action.destination.is_back |
| 1332 | crateback; |
| 1333 | return; |
| 1334 | |
| 1335 | |
| 1336 | if action.destination.is_local |
| 1337 | out.push_str; |
| 1338 | // The payload travels with it, for `by_host`'s reason: dropping it here |
| 1339 | // would be the description saying something the markup then did not |
| 1340 | // carry, and there is no other place to read it from. |
| 1341 | if !action.params.is_empty |
| 1342 | out.push_str; |
| 1343 | json_object_attr; |
| 1344 | out.push; |
| 1345 | |
| 1346 | // Deliberately not `awaiting_attrs`. A wait is a fact about a call and |
| 1347 | // this is not one; a local action that says it waits is a description |
| 1348 | // bug, and drawing a spinner for it would hide that rather than show it. |
| 1349 | return; |
| 1350 | |
| 1351 | |
| 1352 | // The address, built once. The `href` below and the verb further down are |
| 1353 | // the same string whenever both are emitted -- a read of a route names one |
| 1354 | // place -- and building it twice was a `String` per link on every screen |
| 1355 | // made mostly of links. |
| 1356 | let url = route_url; |
| 1357 | |
| 1358 | // The host makes this call, so the renderer hands it the address and emits |
| 1359 | // no transport at all: no verb, no trigger, no `href`. `a81384d4`, and the |
| 1360 | // same shape `saves` already has -- a named attribute the host acts on, |
| 1361 | // rather than a class plus positional arguments. |
| 1362 | // |
| 1363 | // `data-sends` and not `data-action`: what is named is where the thing goes, |
| 1364 | // which is the half the description knows. How it gets there is the host's |
| 1365 | // and is exactly what is not being described. |
| 1366 | if action.by_host |
| 1367 | out.push_str; |
| 1368 | escape_into; |
| 1369 | out.push; |
| 1370 | // The payload travels with it. Dropping it here would be the |
| 1371 | // description saying something the markup then did not carry, and the |
| 1372 | // host has no other place to read it from. |
| 1373 | if !action.params.is_empty |
| 1374 | out.push_str; |
| 1375 | json_object_attr; |
| 1376 | out.push; |
| 1377 | |
| 1378 | // The wait is still a fact about the call and the host is what draws it, |
| 1379 | // so the mark goes on. The `hx-disable` half does not: locking the |
| 1380 | // control is htmx's doing on a request htmx is making, and this is not |
| 1381 | // one. |
| 1382 | awaiting_attrs; |
| 1383 | return; |
| 1384 | |
| 1385 | |
| 1386 | // The answer belongs in a mount of its own, and a mount here is a second |
| 1387 | // document. Opening one is the host's -- a webview cannot put up a window -- |
| 1388 | // so this emits the address and no transport, exactly the shape `by_host` |
| 1389 | // has above. goingson `3fb2526a`. |
| 1390 | // |
| 1391 | // `data-mount` and not `data-sends`: the two say different things to the |
| 1392 | // host and a host that conflated them would post where it should open. What |
| 1393 | // is named is still only where the thing goes. |
| 1394 | // |
| 1395 | // Checked after `by_host` because a call the host performs is already the |
| 1396 | // host's to place; marking such a call `elsewhere` says nothing this |
| 1397 | // renderer can add. |
| 1398 | if action.elsewhere |
| 1399 | out.push_str; |
| 1400 | escape_into; |
| 1401 | out.push; |
| 1402 | // The payload travels with it, for the reason it travels with the two |
| 1403 | // branches above: the description said it, and the host has nowhere |
| 1404 | // else to read it from. |
| 1405 | if !action.params.is_empty |
| 1406 | out.push_str; |
| 1407 | json_object_attr; |
| 1408 | out.push; |
| 1409 | |
| 1410 | // No wait mark. Putting up a mount is not a call that resolves, so a |
| 1411 | // spinner on the control would be waiting for something that never |
| 1412 | // lands. |
| 1413 | return; |
| 1414 | |
| 1415 | |
| 1416 | // The whole document is being replaced, so the browser is what replaces it. |
| 1417 | // The anchor goes on and nothing else does: no verb, no trigger, no swap. |
| 1418 | // An htmx request here would fetch a whole screen and morph it into the page |
| 1419 | // it was meant to leave, which is the failure `00ee7af5` was ruled on. |
| 1420 | // |
| 1421 | // A narrowing rather than an addition. The `href` below is emitted for every |
| 1422 | // read of a route already, for middle-click, copy-link, a crawler and the |
| 1423 | // page with JS off; this is the same anchor with the transport beside it |
| 1424 | // dropped. |
| 1425 | // |
| 1426 | // Guarded on the same three conditions that anchor is, so the mark never |
| 1427 | // turns a call into something it was not. A write marked this way is still |
| 1428 | // performed, because an anchor would ask where the description said to tell, |
| 1429 | // and a load or live trigger has no gesture to follow a link with. |
| 1430 | // |
| 1431 | // Named as the one gesture rather than as a list of exclusions, and that is |
| 1432 | // a correction. It read `!matches!(fires, Load | Live | ClickInStrip)`, |
| 1433 | // which let every gesture that is not a click through -- and the branch |
| 1434 | // *returns*, so a navigating read on a `Fires::ChangeInside` wrapper got an |
| 1435 | // `href` on a `<div>` and no transport at all. `Chrome::band`'s search box |
| 1436 | // is exactly that shape and did nothing. An `href` is only meaningful on |
| 1437 | // the element a browser follows, which is the anchor `control_tag` emits |
| 1438 | // for a click, so the guard says so. |
| 1439 | // |
| 1440 | // The last two guards are the attributes an anchor cannot carry the meaning |
| 1441 | // of. `hx-confirm` is htmx gating the request, so leaving htmx out would |
| 1442 | // drop a question the description asked before acting; `download` and |
| 1443 | // `data-saves` say the answer is a file, which is the opposite of replacing |
| 1444 | // the document. Either one and the transport is emitted as it was, since a |
| 1445 | // dropped prompt and a navigation to a file are both worse than a mark that |
| 1446 | // did nothing. |
| 1447 | if action.navigates |
| 1448 | && matches! |
| 1449 | && !action.method.mutates |
| 1450 | && matches! |
| 1451 | && confirm.is_none |
| 1452 | && action.saves.is_none |
| 1453 | |
| 1454 | out.push_str; |
| 1455 | escape_into; |
| 1456 | out.push; |
| 1457 | return; |
| 1458 | |
| 1459 | |
| 1460 | // A read of a route this app answers is a link, and it gets the address as |
| 1461 | // well as the transport. htmx uses `hx-get` and prevents the default, so |
| 1462 | // the `href` is what everything else uses: middle-click, copy-link, a |
| 1463 | // crawler, and the page with JS off. The parameters are folded into it |
| 1464 | // because a link to a filtered list that drops the filter is a different |
| 1465 | // place, and `hx-vals` below carries the same ones down htmx's path. |
| 1466 | if matches! |
| 1467 | && !action.method.mutates |
| 1468 | && matches! |
| 1469 | |
| 1470 | out.push_str; |
| 1471 | escape_into; |
| 1472 | out.push; |
| 1473 | |
| 1474 | |
| 1475 | // Everything the screen's selection has ticked, gathered by the selector |
| 1476 | // the caller built. `5f2b8753`: this is the whole of what the per-app JS |
| 1477 | // used to do, and it is declarative because a checkbox already submits its |
| 1478 | // own name and value -- all that was missing was something saying which |
| 1479 | // boxes belong together. |
| 1480 | // |
| 1481 | // Here rather than in `act_html` because it is htmx, and htmx entering this |
| 1482 | // crate anywhere else is what the architectural test forbids. |
| 1483 | // |
| 1484 | // One attribute, however many things are being gathered. A field writing |
| 1485 | // over a selection wants both its own control and the ticks, and `fires` |
| 1486 | // below carries the first as a selector of its own; two `hx-include` |
| 1487 | // attributes on one element is markup a parser drops half of, so they are |
| 1488 | // joined here rather than emitted in two places that cannot see each other. |
| 1489 | let include = match |
| 1490 | => None, |
| 1491 | | => Some, |
| 1492 | => Some, |
| 1493 | ; |
| 1494 | if let Some = &include |
| 1495 | out.push_str; |
| 1496 | escape_into; |
| 1497 | out.push; |
| 1498 | |
| 1499 | |
| 1500 | // Where the answer goes, when the responder is not ours to ask. Emitted |
| 1501 | // before the verb so the attributes read in the order they are reasoned |
| 1502 | // about: where it lands, then what is sent. |
| 1503 | match &action.replaces |
| 1504 | None => |
| 1505 | Some => |
| 1506 | out.push_str; |
| 1507 | escape_into; |
| 1508 | out.push; |
| 1509 | |
| 1510 | // `closest [data-row]` rather than a class, for `data-act`'s reason: an |
| 1511 | // attribute survives a host setting `Emit::class_prefix`, and this |
| 1512 | // function does not take `Emit` and so could not build the prefixed |
| 1513 | // selector anyway. One attribute across list rows and table rows, the |
| 1514 | // same choice `data-menu="row"` already made, so a screen mixing the |
| 1515 | // two needs one selector rather than two that look alike. |
| 1516 | // |
| 1517 | // `outerHTML` because "replaces the row I am in" means the row element |
| 1518 | // goes. The default swap would nest the answer inside the row it was |
| 1519 | // meant to replace. |
| 1520 | Some => |
| 1521 | out.push_str; |
| 1522 | |
| 1523 | // A named hook the host acts on, in the same spirit as `data-saves` and |
| 1524 | // for the same reason: htmx has no request-side spelling for "what is |
| 1525 | // showing is now stale". The response header `HX-Refresh` is the |
| 1526 | // server's half, and these acts call routes that answer with a status. |
| 1527 | Some => |
| 1528 | out.push_str; |
| 1529 | |
| 1530 | |
| 1531 | |
| 1532 | // The answer is a file the reader keeps, not a view. On a link the browser |
| 1533 | // does the whole job from the attribute, so nothing else is needed and the |
| 1534 | // control still works with JS off. On a write it cannot: a response has to |
| 1535 | // be performed before it can be saved, so this is a named hook the host |
| 1536 | // acts on, in the same spirit as `data-act` and for the same reason it is an |
| 1537 | // attribute rather than a class. The host handles one attribute instead of |
| 1538 | // a per-button behaviour named by a class and two positional arguments. |
| 1539 | if let Some = &action.saves |
| 1540 | if is_link |
| 1541 | out.push_str; |
| 1542 | else |
| 1543 | out.push_str; |
| 1544 | |
| 1545 | escape_into; |
| 1546 | out.push; |
| 1547 | |
| 1548 | |
| 1549 | let verb = match action.method |
| 1550 | Get => " hx-get=\"", |
| 1551 | Post => " hx-post=\"", |
| 1552 | Delete => " hx-delete=\"", |
| 1553 | Put => " hx-put=\"", |
| 1554 | ; |
| 1555 | out.push_str; |
| 1556 | escape_into; |
| 1557 | out.push; |
| 1558 | |
| 1559 | if chooses |
| 1560 | // `1894e95d`. A row of a live selection sends what the press *meant* |
| 1561 | // beside whatever the address already carries, and only the browser can |
| 1562 | // say what it meant, so the object is computed at click time rather than |
| 1563 | // written now. htmx allows one `hx-vals`, so the static half is folded |
| 1564 | // into the same literal rather than emitted twice. |
| 1565 | // |
| 1566 | // `metaKey` beside `ctrlKey` because a Mac browser reports command as |
| 1567 | // the former and every file manager on that platform toggles with it. |
| 1568 | // Shift wins, matching the desktop everywhere: a range is the bigger |
| 1569 | // statement, and holding both cannot mean the plain press. |
| 1570 | out.push_str; |
| 1571 | for in action.params.iter |
| 1572 | escape_into; |
| 1573 | |
| 1574 | out.push_str; |
| 1575 | escape_into; |
| 1576 | out.push_str; |
| 1577 | else if !action.params.is_empty |
| 1578 | out.push_str; |
| 1579 | json_object_attr; |
| 1580 | out.push; |
| 1581 | |
| 1582 | |
| 1583 | // Named even where it matches htmx's own default for the element, so the |
| 1584 | // markup says what it does rather than resting on a default holding. |
| 1585 | match fires |
| 1586 | // htmx's default for the element in both cases: a click for a button or |
| 1587 | // a link, a submit for a form. |
| 1588 | Click | ClickInStrip | Submit => |
| 1589 | // `data-act` and not the class, so the filter does not depend on |
| 1590 | // `Emit::class_prefix` and does not break when a host sets one. Same |
| 1591 | // reasoning as `data-menu` on a row's menu. |
| 1592 | ClickBeside => out.push_str |
| 1593 | " hx-trigger=\"click[!event.target.closest(", |
| 1594 | "'[data-act]')]\"" |
| 1595 | , |
| 1596 | Change => out.push_str, |
| 1597 | // Once, when the element appears. `Slot::fed_by` is the whole of what |
| 1598 | // reaches this, and the region it sits on is `Readiness::Pending` |
| 1599 | // already, so the stand-in is on screen before the request leaves. |
| 1600 | Load => out.push_str, |
| 1601 | // `load` first, then the interval. htmx waits out a whole period before |
| 1602 | // an `every` fires, so the pair is what makes the first paint arrive at |
| 1603 | // the same time it did before the region became live. |
| 1604 | Live => |
| 1605 | out.push_str; |
| 1606 | out.push_str; |
| 1607 | out.push_str; |
| 1608 | |
| 1609 | Key => |
| 1610 | // `from:body`, because the element is hidden and never focused: a |
| 1611 | // trigger on itself would wait for a keystroke it can never |
| 1612 | // receive. |
| 1613 | out.push_str; |
| 1614 | escape_into; |
| 1615 | out.push_str; |
| 1616 | |
| 1617 | // Its `hx-include` is emitted above with the ticks, if there are any. |
| 1618 | ChangeInside => out.push_str, |
| 1619 | // `changed` so that arrow keys and Tab do not ask the same question |
| 1620 | // again, and the delay so that a name is asked about once rather than |
| 1621 | // once per letter. Milliseconds because that is htmx's unit; the |
| 1622 | // description carries a `Duration` so the terminal renderer is not |
| 1623 | // handed a number in someone else's units. |
| 1624 | // The dials that are not typed into: `input` covers the radio and the |
| 1625 | // select `keyup` never hears, and does not fire on blur the way |
| 1626 | // `change` would. `Fires::Working` carries the argument. |
| 1627 | Working => |
| 1628 | out.push_str; |
| 1629 | if at_least > 0 |
| 1630 | out.push_str; |
| 1631 | out.push_str; |
| 1632 | out.push; |
| 1633 | |
| 1634 | out.push_str; |
| 1635 | out.push_str; |
| 1636 | out.push_str; |
| 1637 | |
| 1638 | Typing => |
| 1639 | out.push_str; |
| 1640 | // htmx takes the filter on the event and the modifiers after it, so |
| 1641 | // the floor goes here rather than beside the delay. `event.target` |
| 1642 | // and not `this`: the trigger sits on the wrapper and the value |
| 1643 | // being typed belongs to the control inside it, which is the same |
| 1644 | // reason `include` above has to find the control back. |
| 1645 | if at_least > 0 |
| 1646 | out.push_str; |
| 1647 | out.push_str; |
| 1648 | out.push; |
| 1649 | |
| 1650 | out.push_str; |
| 1651 | out.push_str; |
| 1652 | out.push_str; |
| 1653 | |
| 1654 | |
| 1655 | |
| 1656 | // Asking before acting is transport here, same as the verb: htmx gates the |
| 1657 | // request on it. That is also why it lands in this function rather than |
| 1658 | // beside the label — every `hx-` attribute this crate emits comes from one |
| 1659 | // place, or swapping htmx for fixi stops being a one-function change. |
| 1660 | if let Some = confirm |
| 1661 | out.push_str; |
| 1662 | escape_into; |
| 1663 | out.push; |
| 1664 | |
| 1665 | |
| 1666 | // Decision 7's slack: a morph preserves focus, scroll and input state |
| 1667 | // through a swap, so a whole-Screen answer stops being destructive. |
| 1668 | // |
| 1669 | // Unconditional since the emitter moved to htmx 4, where `outerMorph` is a |
| 1670 | // swap style rather than a swap style an extension has to supply. Under 2.x |
| 1671 | // this was emitted only when the shell loaded idiomorph, because htmx fell |
| 1672 | // back to `innerHTML` in silence when it did not -- and silently |
| 1673 | // destructive is the outcome decision 7 exists to avoid. There is nothing |
| 1674 | // left to be missing. `outerMorph` and not `innerMorph`: what is replaced |
| 1675 | // is the target itself, which is what the extension's bare `morph` meant. |
| 1676 | out.push_str; |
| 1677 | |
| 1678 | // The action says it waits on something that resolves once, so the control |
| 1679 | // stops answering until it does. `hx-disable` is the half of this |
| 1680 | // treatment the MNW server writes twice against 57 spinners, and it is the |
| 1681 | // half that guards a double-submitted purchase. |
| 1682 | // |
| 1683 | // `hx-disable` is htmx 4's name for what 2.x called `hx-disabled-elt`. The |
| 1684 | // name 2.x gave `hx-disable` -- skip processing this subtree -- is |
| 1685 | // `hx-ignore` in 4, and nothing here emits it. |
| 1686 | // |
| 1687 | // No `hx-indicator`: htmx already puts `htmx-request` on the element that |
| 1688 | // made the request, so naming it as its own indicator emits an attribute |
| 1689 | // that changes nothing. A screen wanting a spinner somewhere else is |
| 1690 | // pointing at an element no description names. |
| 1691 | // |
| 1692 | // No static `aria-busy` either. The control is not busy when the page is |
| 1693 | // painted, and an attribute that is true only between two events is the |
| 1694 | // binder's to set. What a reader gets in the meantime is the `disabled` htmx |
| 1695 | // applies, which is not silent. |
| 1696 | if action.awaiting.is_some |
| 1697 | // What gets locked is the trigger's to say, not always `this`: see |
| 1698 | // `Fires::guards`. |
| 1699 | out.push_str; |
| 1700 | out.push_str; |
| 1701 | out.push; |
| 1702 | awaiting_attrs; |
| 1703 | |
| 1704 | |
| 1705 | |
| 1706 | /// What a control says about the wait it is in, without saying who locks it. |
| 1707 | /// |
| 1708 | /// `hx-disable` stayed at the call site because it needs the trigger, which is |
| 1709 | /// the renderer's business either way. |
| 1710 | |
| 1711 | let Some = &action.awaiting else |
| 1712 | return; |
| 1713 | ; |
| 1714 | out.push_str; |
| 1715 | out.push_str |
| 1716 | "determinate" |
| 1717 | else |
| 1718 | "indeterminate" |
| 1719 | ; |
| 1720 | out.push; |
| 1721 | // The amount, when it was measured. Never a duration and never anything |
| 1722 | // derived into one: a bar drawn from this shows what is done over what |
| 1723 | // there is, and the time it has taken, and predicts nothing. |
| 1724 | if let Some = awaiting.amount |
| 1725 | let _ = write!; |
| 1726 | |
| 1727 | |
| 1728 | |
| 1729 | /// Whether an action is somewhere to go rather than something to do. |
| 1730 | /// |
| 1731 | /// Two ways to be a link. An external destination leaves. A read of a route |
| 1732 | /// this app answers is also a link: it has an address, it can be visited |
| 1733 | /// directly, and nothing changes because it was. |
| 1734 | /// |
| 1735 | /// A write is never a link however it is spelled, which is the whole of the |
| 1736 | /// other side. An anchor is something a browser may prefetch and a crawler will |
| 1737 | /// follow, and neither is allowed to delete a task. |
| 1738 | /// |
| 1739 | /// A local destination is neither way. There is nowhere to go, so an anchor |
| 1740 | /// would carry no address and a middle-click would open a copy of the page the |
| 1741 | /// reader is already on. It is a button, whatever its method says: the method |
| 1742 | /// is meaningless once nothing is asked, the same way it is on a |
| 1743 | /// [`Destination::External`]. |
| 1744 | const |
| 1745 | if action.destination.is_local |
| 1746 | return false; |
| 1747 | |
| 1748 | action.destination.is_external || !action.method.mutates |
| 1749 | |
| 1750 | |
| 1751 | /// The element a control becomes. |
| 1752 | /// |
| 1753 | /// A link is an anchor and a write is a button, and a button that navigates is |
| 1754 | /// a button lying to everything that reads the page: middle-click, copy-link, |
| 1755 | /// a crawler and a screen reader included. This keyed on external-or-not until |
| 1756 | /// the read case was separated out, which made every internal navigation a |
| 1757 | /// control that only worked by running JavaScript first. |
| 1758 | /// |
| 1759 | /// Branching on the [`Destination`] variant and never on the shape of the |
| 1760 | /// string is the rule `Destination`'s own docs set. "Starts with https" is how a |
| 1761 | /// route named `/https-setup` ends up opening a browser. |
| 1762 | const |
| 1763 | if is_link |
| 1764 | |
| 1765 | else |
| 1766 | |
| 1767 | |
| 1768 | |
| 1769 | |
| 1770 | /// A control that calls a route, with whatever it asks for first around it. |
| 1771 | pub |
| 1772 | if act.asks.is_empty |
| 1773 | control_html; |
| 1774 | anchor_slot; |
| 1775 | return; |
| 1776 | |
| 1777 | |
| 1778 | // `Act::asks`, the disclosure half. MNW's bulk bar presses "Set Price" and |
| 1779 | // reveals a box, an Apply and a hint, all of it hidden behind a `hidden` |
| 1780 | // class and 40 lines of JS toggling it. `details` is what a browser already |
| 1781 | // has for that: it opens on the press, closes on the second one, keeps its |
| 1782 | // own state across a swap of anything outside it, and needs no script. |
| 1783 | // |
| 1784 | // The summary carries the verb and the control inside carries it again, |
| 1785 | // rather than the inner one being called "Apply". A described act has one |
| 1786 | // label, and inventing a second word here would be this renderer writing |
| 1787 | // copy. The count the selection script appends lands on the inner control, |
| 1788 | // which is the one that fires. |
| 1789 | out.push_str; |
| 1790 | class_attr; |
| 1791 | out.push; |
| 1792 | out.push_str; |
| 1793 | class_attr; |
| 1794 | out.push; |
| 1795 | escape_into; |
| 1796 | out.push_str; |
| 1797 | class_attr; |
| 1798 | out.push; |
| 1799 | for field in &act.asks |
| 1800 | // `as_asked`: a question here is answered by the control below it, so |
| 1801 | // the write members are dropped rather than honoured. A box that also |
| 1802 | // wrote on its own would send the value twice, once as it was typed and |
| 1803 | // once when the verb fired. |
| 1804 | field_group_html; |
| 1805 | |
| 1806 | control_html; |
| 1807 | out.push_str; |
| 1808 | anchor_slot; |
| 1809 | |
| 1810 | |
| 1811 | /// The popover container belonging to a named control, if it is named. |
| 1812 | /// |
| 1813 | /// After the control and outside it: an anchored screen is drawn *at* the |
| 1814 | /// control, and a menu inside a `button` is markup a browser is entitled to |
| 1815 | /// discard. That is the same placement a row's menu takes for the same reason. |
| 1816 | |
| 1817 | if let Some = act.id.as_deref.and_then |
| 1818 | anchor_container_html; |
| 1819 | |
| 1820 | |
| 1821 | |
| 1822 | /// The control itself: the label, the address, and what it gathers. |
| 1823 | |
| 1824 | // The commit control for a staged selection gathers every tick on the |
| 1825 | // screen. One selector rather than a name per box: a screen holds one set |
| 1826 | // (`Screen::selection`), so the class the ticks already carry is what they |
| 1827 | // have in common. Built here because it needs `Emit`, which the transport |
| 1828 | // function does not take -- a host setting `class_prefix` moves the class |
| 1829 | // and the selector together. |
| 1830 | // |
| 1831 | // Built inside the `map` and not before it: staged selections are rare and |
| 1832 | // every other control on the screen was paying for two `String`s it then |
| 1833 | // dropped. `class` rather than `class_into` because what is wanted here is |
| 1834 | // a value, and a selector is the one place this crate holds one. |
| 1835 | // |
| 1836 | // A control that asked for a value gathers that too, by the disclosure it |
| 1837 | // sits in. `closest details` rather than an id: nothing in this renderer |
| 1838 | // mints ids, and the boxes a press should send are exactly the ones inside |
| 1839 | // the `details` the press happened in. |
| 1840 | let mut selectors: = Vecnew; |
| 1841 | if act.over.is_some |
| 1842 | selectors.push; |
| 1843 | |
| 1844 | if !act.asks.is_empty |
| 1845 | selectors.push; |
| 1846 | |
| 1847 | let gathers = .then; |
| 1848 | let gathers = gathers.as_deref; |
| 1849 | // `button`, which is makeover's name for this and carries its whole |
| 1850 | // interactive set: the raised bevel, the hover fill, the pressed inset, the |
| 1851 | // focus ring and the disabled treatment. This emitted `act`, a second name |
| 1852 | // for the same thing that no stylesheet in the tree defined, so a described |
| 1853 | // control rendered as unstyled text. There was never a concept here that |
| 1854 | // `button` was not already the word for. |
| 1855 | let = control_tag; |
| 1856 | out.push_str; |
| 1857 | class_attr; |
| 1858 | tone_attr; |
| 1859 | // Named rather than found by class, so anything binding to "this is a |
| 1860 | // control" survives a host setting `Emit::class_prefix`. `Fires::ClickBeside` |
| 1861 | // is the first reader; a table row uses it to tell its own click apart from |
| 1862 | // a press on a button sitting inside one of its cells. |
| 1863 | out.push_str; |
| 1864 | |
| 1865 | // `ae8e8836`. The control's own name in the document, so the container |
| 1866 | // emitted beside it has something to be positioned against. Emitted only |
| 1867 | // when the description gave one: `Act::id` is `None` on nearly every |
| 1868 | // control, and a renderer minting ids for the rest would be inventing |
| 1869 | // addresses nothing asked for. |
| 1870 | if let Some = &act.id |
| 1871 | out.push_str; |
| 1872 | escape_into; |
| 1873 | out.push; |
| 1874 | |
| 1875 | |
| 1876 | // The hook the selection script reads: this control acts on the set, so it |
| 1877 | // says how many are in it and goes inert when there are none. Named rather |
| 1878 | // than a class for `data-act`'s reason, and it carries the selection's name |
| 1879 | // even though nothing selects between sets yet -- `Act::over` explains why |
| 1880 | // that day is not this one, and an attribute that already holds the name |
| 1881 | // costs nothing to start reading. |
| 1882 | if let Some = &act.over |
| 1883 | out.push_str; |
| 1884 | escape_into; |
| 1885 | out.push; |
| 1886 | |
| 1887 | |
| 1888 | // Where a chosen value goes, and what goes there. `f35aafee`. Two escaped |
| 1889 | // attributes rather than an emitted program: the value is app text -- a |
| 1890 | // media file's name arrives inside it -- and `crate::hyperscript`'s one |
| 1891 | // standing rule is that no program is built out of any. `crate::FILL_JS` |
| 1892 | // reads both. |
| 1893 | // |
| 1894 | // Emitted whatever the state is, unlike the transport below. A disabled |
| 1895 | // control performs nothing because it reports no click, so the guard is the |
| 1896 | // element rather than the absence of the attribute, and a control that is |
| 1897 | // re-enabled still knows where it deposits. |
| 1898 | if let Some = &act.fills |
| 1899 | out.push_str; |
| 1900 | escape_into; |
| 1901 | out.push_str; |
| 1902 | escape_into; |
| 1903 | out.push; |
| 1904 | |
| 1905 | |
| 1906 | match act.state |
| 1907 | Some => |
| 1908 | // Disabled and emitting no transport, rather than disabled and |
| 1909 | // still carrying the address. A control that stops answering input |
| 1910 | // should also stop being a request waiting to be re-enabled from |
| 1911 | // the console. |
| 1912 | // |
| 1913 | // An anchor has no `disabled`, and omitting the href is what |
| 1914 | // actually stops it: an `<a>` without one is not a link, so it |
| 1915 | // drops out of the tab order on its own. `aria-disabled` is what |
| 1916 | // says why, since a bare span-shaped anchor says nothing. |
| 1917 | if is_link |
| 1918 | out.push_str; |
| 1919 | else |
| 1920 | out.push_str; |
| 1921 | |
| 1922 | |
| 1923 | // No `autofocus` arm. A description does not state focus: the browser |
| 1924 | // owns reach and focus here, which is what `makeover-layout` 0.19.0 |
| 1925 | // settled by removing the member this used to read. |
| 1926 | // |
| 1927 | // `State` is `#[non_exhaustive]`, so a member added upstream lands |
| 1928 | // here. Emitting the transport is the right default for anything that |
| 1929 | // is not a suppression: a state this renderer has not learned yet |
| 1930 | // should leave the control working, not silently inert. |
| 1931 | _ => action_attrs |
| 1932 | &act.action, |
| 1933 | Click, |
| 1934 | act.confirm.as_deref, |
| 1935 | gathers, |
| 1936 | false, |
| 1937 | out, |
| 1938 | , |
| 1939 | |
| 1940 | |
| 1941 | // The confirmation rides with the transport, in `action_attrs`. The key does |
| 1942 | // not: `accesskey` is plain HTML and no part of htmx. It is emitted even |
| 1943 | // though a browser makes little of it, because a key is the affordance in a |
| 1944 | // terminal and this is the nearest honest thing a page has. |
| 1945 | if let Some = &act.key |
| 1946 | out.push_str; |
| 1947 | escape_into; |
| 1948 | out.push; |
| 1949 | |
| 1950 | |
| 1951 | // `ca7b5200`. Standing help, as `title` *and* as `aria-description`. `title` |
| 1952 | // alone is the mistake the shipped templates made 90 times: it is a hover, so |
| 1953 | // a touch reader never sees it and several screen readers are configured not |
| 1954 | // to announce it. Both attributes say the same sentence to the two audiences |
| 1955 | // that read different ones. |
| 1956 | if let Some = &act.hint |
| 1957 | out.push_str; |
| 1958 | escape_into; |
| 1959 | out.push_str; |
| 1960 | escape_into; |
| 1961 | out.push; |
| 1962 | |
| 1963 | |
| 1964 | // `c3e145e0`. The value rides in an escaped attribute and `COPY_JS` reads |
| 1965 | // it, which is `FILL_JS`' arrangement and for its reason: the string is app |
| 1966 | // text -- a licence key, an embed snippet, a feed URL -- and no program in |
| 1967 | // this renderer is built out of text a user typed. |
| 1968 | // |
| 1969 | // The act's own action is emitted beside this and is a local one, so |
| 1970 | // nothing is asked and no transport attribute appears. A host that does not |
| 1971 | // serve the script gets a button that does nothing, which is why the |
| 1972 | // constant's docs say so. |
| 1973 | if let Some = &act.copies |
| 1974 | out.push_str; |
| 1975 | escape_into; |
| 1976 | out.push; |
| 1977 | |
| 1978 | |
| 1979 | out.push; |
| 1980 | // `db998898`. What the control shows, before what it says: a thumbnail with |
| 1981 | // its name under it is the shape both measured sites have, and the label |
| 1982 | // stays in the markup rather than moving to an `aria-label`, so the control |
| 1983 | // has accessible text whether or not the picture arrives. |
| 1984 | if let Some = &act.shows |
| 1985 | img_tag; |
| 1986 | |
| 1987 | escape_into; |
| 1988 | out.push_str; |
| 1989 | |
| 1990 | |
| 1991 | /// The `<img>` itself, without the `<figure>` a caption would want. |
| 1992 | |
| 1993 | out.push_str; |
| 1994 | class_attr; |
| 1995 | out.push_str; |
| 1996 | // Escaped as an attribute and otherwise untouched, the treatment every |
| 1997 | // app-supplied string gets here. No scheme guard: quoting is what stops an |
| 1998 | // attribute breaking out, and unlike `href` an `src` has no scheme that |
| 1999 | // executes -- `javascript:` in an `<img src>` is a broken picture, not a |
| 2000 | // script. |
| 2001 | escape_into; |
| 2002 | out.push_str; |
| 2003 | escape_into; |
| 2004 | out.push; |
| 2005 | |
| 2006 | // The picture's own dimensions, which is how the browser holds its place. |
| 2007 | // With `height: auto` set by makeover, these two attributes give the box an |
| 2008 | // aspect ratio before a byte arrives, so the space is right at any width and |
| 2009 | // nothing below moves when the image lands. Omitted when the app does not |
| 2010 | // know them -- an invented size would reserve the wrong room, which is worse |
| 2011 | // than none. |
| 2012 | if let Some = picture.intrinsic |
| 2013 | let _ = write!; |
| 2014 | |
| 2015 | |
| 2016 | // 0.21.0 wrote `loading="lazy"` for every picture and that was wrong for |
| 2017 | // anything on screen at first paint: deferring what is already needed saves |
| 2018 | // nothing and lands its arrival later, so the page moves more rather than |
| 2019 | // less. The description says which it is now, and eager is the default. |
| 2020 | if matches! |
| 2021 | out.push_str; |
| 2022 | |
| 2023 | fit_attr; |
| 2024 | out.push; |
| 2025 | |
| 2026 | |
| 2027 | /// One row of a list. |
| 2028 | /// |
| 2029 | /// `folded` is [`quasi_router::folded`]'s answer for this row: whether a shut |
| 2030 | /// branch above it is hiding it. The row is emitted regardless and marked |
| 2031 | /// `hidden`, which is what lets `outline.js` open a branch without asking the |
| 2032 | /// app for rows the document is already holding. |
| 2033 | /// How a row is drawn: as a list item, or as a row of a declared table. |
| 2034 | /// |
| 2035 | /// The 2026-09-05 collapse made a list row and a table row one type, and this |
| 2036 | /// is what is genuinely left of the difference. It is not a preference: the |
| 2037 | /// element, the class names and the gutter's wrapping are a contract with |
| 2038 | /// makeover's stylesheet and with the hosts that bind to them, so the two looks |
| 2039 | /// keep emitting exactly what they emitted before. What they no longer do is |
| 2040 | /// keep two copies of the walk that decides it. |
| 2041 | /// |
| 2042 | /// Which look a container takes is the container's, not the row's, and since |
| 2043 | /// the 2026-09-06 collapse there is one container: it is whether the table |
| 2044 | /// declared any columns. This doc predicted that a wave early and guessed the |
| 2045 | /// test slightly wrong -- it is whether there are columns at all, not whether |
| 2046 | /// they carry headings. |
| 2047 | |
| 2048 | |
| 2049 | /// A list item. Flows its cells as spans on one wrapped line. |
| 2050 | List, |
| 2051 | /// A row of a declared table. Emits a cell per column, aligned by |
| 2052 | /// `display: table`. |
| 2053 | Table, |
| 2054 | |
| 2055 | |
| 2056 | |
| 2057 | /// The opening tag, without its attributes. |
| 2058 | const |
| 2059 | match self |
| 2060 | SelfList => "<li", |
| 2061 | SelfTable => "<div role=\"row\"", |
| 2062 | |
| 2063 | |
| 2064 | |
| 2065 | /// The closing tag. |
| 2066 | const |
| 2067 | match self |
| 2068 | SelfList => "</li>", |
| 2069 | SelfTable => "</div>", |
| 2070 | |
| 2071 | |
| 2072 | |
| 2073 | /// The class every row of this look carries. |
| 2074 | const |
| 2075 | match self |
| 2076 | SelfList => "row", |
| 2077 | SelfTable => "table-row", |
| 2078 | |
| 2079 | |
| 2080 | |
| 2081 | /// The class for the row the detail side is showing. |
| 2082 | const |
| 2083 | match self |
| 2084 | SelfList => "row-current", |
| 2085 | SelfTable => "table-row-current", |
| 2086 | |
| 2087 | |
| 2088 | |
| 2089 | /// The class for a row in a selection already in force. |
| 2090 | const |
| 2091 | match self |
| 2092 | SelfList => "row-chosen", |
| 2093 | SelfTable => "table-row-chosen", |
| 2094 | |
| 2095 | |
| 2096 | |
| 2097 | /// The class for a ticked row. |
| 2098 | const |
| 2099 | match self |
| 2100 | SelfList => "row-selected", |
| 2101 | SelfTable => "table-row-selected", |
| 2102 | |
| 2103 | |
| 2104 | |
| 2105 | |
| 2106 | /// The classes a row carries, in the order this emitter has always written |
| 2107 | /// them. |
| 2108 | /// |
| 2109 | /// Shared by both looks since 2026-09-05. `row-nested` and `row-branch` were |
| 2110 | /// already spelled the same on both, which is the tell that the two functions |
| 2111 | /// were one function. |
| 2112 | |
| 2113 | let mut classes = vec!; |
| 2114 | if row.depth.is_nested |
| 2115 | classes.push; |
| 2116 | |
| 2117 | if row.open.is_some |
| 2118 | classes.push; |
| 2119 | |
| 2120 | if row.current |
| 2121 | // `1894e95d`. A live selection, which is neither of the two below it: |
| 2122 | // not the app's pointer at one row and not a staged tick. Its own class |
| 2123 | // rather than reusing the tick's, because the tick's class is what a |
| 2124 | // commit control gathers by and these rows have nothing to commit. |
| 2125 | classes.push; |
| 2126 | |
| 2127 | if row.chosen == Some |
| 2128 | classes.push; |
| 2129 | |
| 2130 | if row.selected == Some |
| 2131 | classes.push; |
| 2132 | |
| 2133 | classes |
| 2134 | |
| 2135 | |
| 2136 | /// The row's own attributes, from `data-row` to the `>` that closes the tag. |
| 2137 | /// |
| 2138 | /// Shared by both looks. Every attribute here was written twice before |
| 2139 | /// 2026-09-05, in two functions that had drifted: the table row emitted |
| 2140 | /// `data-value` and `data-change` and the list row did not, which is recorded |
| 2141 | /// as a finding on wave `f0dac2d6` rather than silently fixed here, because |
| 2142 | /// changing what a served screen emits is not a refactor. |
| 2143 | |
| 2144 | // `Replaces::Enclosing`'s target. |
| 2145 | out.push_str; |
| 2146 | if look == Table |
| 2147 | // What the app calls this row, unique within its table. A list row |
| 2148 | // carries the same fact and spends it on its checkbox's `value=` |
| 2149 | // instead; see the finding on wave `f0dac2d6`. |
| 2150 | if let Some = &row.value |
| 2151 | out.push_str; |
| 2152 | escape_into; |
| 2153 | out.push; |
| 2154 | |
| 2155 | |
| 2156 | // The address, which is the document's name for the row rather than the |
| 2157 | // app's, and so is the one that gets an `id`. |
| 2158 | if let Some = &row.address |
| 2159 | out.push_str; |
| 2160 | escape_into; |
| 2161 | out.push; |
| 2162 | |
| 2163 | if look == Table |
| 2164 | // Which side of a change this line is on, when the table is a diff. |
| 2165 | // `19d7602d`. An attribute rather than a class, for `tone_attr`'s |
| 2166 | // reason: makeover keys every intent off a data attribute, so a host |
| 2167 | // that has never heard of a diff still gets the tones it styles. |
| 2168 | // |
| 2169 | // `None` writes nothing, which keeps every ordinary table from reading |
| 2170 | // as a diff whose lines are all context. |
| 2171 | if let Some = row.change |
| 2172 | out.push_str; |
| 2173 | out.push_str |
| 2174 | Added => "added", |
| 2175 | Removed => "removed", |
| 2176 | // Including a kind added to this `#[non_exhaustive]` axis that |
| 2177 | // this renderer has not learned: an unknown side reads as an |
| 2178 | // unchanged line, which draws the text and loses only the tint. |
| 2179 | _ => "context", |
| 2180 | ; |
| 2181 | out.push_str; |
| 2182 | out.push_str; |
| 2183 | out.push; |
| 2184 | |
| 2185 | |
| 2186 | if row.depth.is_nested |
| 2187 | let _ = write! |
| 2188 | out, |
| 2189 | " data-depth=\"{depth}\" style=\"--row-depth:{depth}\" aria-level=\"{level}\"" |
| 2190 | depth = row.depth.level, |
| 2191 | level = row.depth.level + 1 |
| 2192 | ); |
| 2193 | |
| 2194 | if folded |
| 2195 | out.push_str; |
| 2196 | |
| 2197 | if row.current |
| 2198 | out.push_str; |
| 2199 | |
| 2200 | if let Some = row.chosen |
| 2201 | out.push_str |
| 2202 | " aria-selected=\"true\"" |
| 2203 | else |
| 2204 | " aria-selected=\"false\"" |
| 2205 | ; |
| 2206 | |
| 2207 | |
| 2208 | |
| 2209 | |
| 2210 | let classes = row_classes; |
| 2211 | out.push_str; |
| 2212 | class_attr; |
| 2213 | row_attrs_html; |
| 2214 | out.push; |
| 2215 | |
| 2216 | // The disclosure, when the row is a branch. Before the tick and before the |
| 2217 | // run, because it is the affordance for the row's place in the outline |
| 2218 | // rather than for the row: a reader scanning down the chevrons is reading |
| 2219 | // the shape of the tree. |
| 2220 | // |
| 2221 | // **A separate hit target from the label**, which is `Row::open`'s |
| 2222 | // instruction and the shipped egui sidebar's own behaviour: pressing a tag |
| 2223 | // filters by it, pressing its chevron does not. A button rather than a span |
| 2224 | // for the tick's reason -- the browser already gives it the keyboard, the |
| 2225 | // space key and a name. |
| 2226 | // |
| 2227 | // No route, and no `hx-` anything. Folding a branch is the reader tidying |
| 2228 | // their own view; see `Row::open` for why that is not a write. |
| 2229 | if let Some = row.open |
| 2230 | out.push_str; |
| 2231 | class_attr; |
| 2232 | out.push_str; |
| 2233 | out.push_str; |
| 2234 | // The words a reader hears. Not a label a description gave: the |
| 2235 | // vocabulary says a row has a disclosure and never says what to call |
| 2236 | // it, so this is the renderer naming its own affordance the way the |
| 2237 | // tick's "Select" is. |
| 2238 | out.push_str; |
| 2239 | out.push_str; |
| 2240 | out.push_str; |
| 2241 | |
| 2242 | |
| 2243 | // The tick, when the row is selectable at all. A real checkbox rather than |
| 2244 | // a styled span: it is the one control here the browser already gets right, |
| 2245 | // including the label association, the space key and the mixed state a |
| 2246 | // screen reader announces. |
| 2247 | if let Some = row.selected |
| 2248 | out.push_str; |
| 2249 | class_attr; |
| 2250 | if ticked |
| 2251 | out.push_str; |
| 2252 | |
| 2253 | // What the tick contributes to the screen's selection, under the one |
| 2254 | // name a handler reads it back by. `5f2b8753`: the app used to bind |
| 2255 | // this itself, gathering the checked boxes in JS, because nothing in |
| 2256 | // the description said what the ticks were for. |
| 2257 | // |
| 2258 | // Named rather than left to the browser's `on`, which is what a |
| 2259 | // checkbox with no value submits. `on` says a box was checked and not |
| 2260 | // which one. |
| 2261 | if let Some = &row.value |
| 2262 | out.push_str; |
| 2263 | escape_into; |
| 2264 | out.push_str; |
| 2265 | escape_into; |
| 2266 | out.push; |
| 2267 | |
| 2268 | // A tick with no route is local state until something submits it, which |
| 2269 | // is what a bulk checkbox is; the app binds those itself. A tick with |
| 2270 | // one is the write, which is `14612ed8`, and htmx's own default trigger |
| 2271 | // for an input is `change` — named anyway, so the markup says what it |
| 2272 | // does rather than relying on a default holding. |
| 2273 | if let Some = &row.toggle |
| 2274 | action_attrs; |
| 2275 | |
| 2276 | out.push_str; |
| 2277 | |
| 2278 | |
| 2279 | // A span per run of consecutive parts sharing a role, rather than a fixed |
| 2280 | // sequence of members. The old shape drew primary, secondary, meta, bar, |
| 2281 | // tokens, actions in that order however the description was built; the run |
| 2282 | // draws what it was given where it was put, and a row with a tag between |
| 2283 | // two facts now says so. |
| 2284 | // |
| 2285 | // Consecutive same-role parts share one wrapping span for the reason a |
| 2286 | // cell's tokens do: the part class carries the gap between siblings, so a |
| 2287 | // span each would space two badges as though they were unrelated. |
| 2288 | // |
| 2289 | // Flow joins the role in the grouping key, and has to: the span is what |
| 2290 | // carries the clamp, so two parts sharing a role and disagreeing about how |
| 2291 | // many lines they may take cannot share one. A row whose title is relaxed |
| 2292 | // and whose second title-role part is not gets two spans, which is the |
| 2293 | // right answer and is also the only one that can be drawn. |
| 2294 | let mut rest = row.cells.as_slice; |
| 2295 | while let Some = rest.first |
| 2296 | let = ; |
| 2297 | let taken = rest |
| 2298 | .iter |
| 2299 | .take_while |
| 2300 | .count; |
| 2301 | let = rest.split_at; |
| 2302 | // A list draws over the default column set, so every cell in one is |
| 2303 | // keyed by role. A cell keyed to a declared column has reached a list |
| 2304 | // row, which is a description error rather than something to render: |
| 2305 | // the table constructors (`Row::cells`, `at`, `cell`) produce those |
| 2306 | // keys and a list row is built by `Row::new` and its siblings. |
| 2307 | // |
| 2308 | // Loud in debug and benign in release, which is the bargain |
| 2309 | // `Table::row`'s two assertions already strike here (`d41d00a`): a |
| 2310 | // panic in a description is worse than a row that draws its text under |
| 2311 | // the wrong style. |
| 2312 | debug_assert! |
| 2313 | matches!, |
| 2314 | "a cell keyed to a declared column reached a list row, which draws \ |
| 2315 | over the default column set and has no column to style it from. \ |
| 2316 | Key: {key:?}", |
| 2317 | ; |
| 2318 | let role = match &key |
| 2319 | Role => *role, |
| 2320 | _ => Primary, |
| 2321 | ; |
| 2322 | row_part_html; |
| 2323 | rest = tail; |
| 2324 | |
| 2325 | |
| 2326 | // After the run, because a menu is not on the line: it is the set of things |
| 2327 | // that can be done to the row, and it renders as a container the host opens |
| 2328 | // its own way. A webview hangs a context menu off it, a touch host an action |
| 2329 | // sheet, a terminal a key-driven list; all three read the same acts. |
| 2330 | if !row.menu.is_empty |
| 2331 | out.push_str; |
| 2332 | class_attr; |
| 2333 | // Named rather than hidden by class, so the host's own menu code has |
| 2334 | // something to bind to that does not depend on how it is styled. |
| 2335 | out.push_str; |
| 2336 | for act in &row.menu |
| 2337 | act_html; |
| 2338 | |
| 2339 | out.push_str; |
| 2340 | |
| 2341 | |
| 2342 | out.push_str; |
| 2343 | |
| 2344 | |
| 2345 | /// One run of consecutive row parts sharing a role. |
| 2346 | |
| 2347 | row: &Row, |
| 2348 | role: RowPart, |
| 2349 | flow: Flow, |
| 2350 | group: &[Cell], |
| 2351 | opts: &Emit, |
| 2352 | out: &mut String, |
| 2353 | |
| 2354 | // The primary is a control when selecting the row does something, and plain |
| 2355 | // text when it does not. Emitting a button either way would give a screen |
| 2356 | // reader an affordance that answers nothing. `activate` stayed a field |
| 2357 | // through the run migration, so this is still the row's own answer rather |
| 2358 | // than something recomputed from a part. |
| 2359 | let activates = role == Primary && row.activate.is_some; |
| 2360 | |
| 2361 | // `None` for a tight part, which is every part that has not asked |
| 2362 | // otherwise: one line is what the run already does and a class saying so |
| 2363 | // would be on every span in every row. |
| 2364 | let clamp = flow_class; |
| 2365 | |
| 2366 | let close = if let = |
| 2367 | let = control_tag; |
| 2368 | out.push_str; |
| 2369 | // The activating primary is a control, and it is still the thing whose |
| 2370 | // lines are being counted. Clamping the span outside it would clip a |
| 2371 | // box the button had already overflowed. |
| 2372 | // |
| 2373 | // Two calls rather than a built-up vec, so the literal stays where the |
| 2374 | // vocabulary test's reader can see it. That test only knows the |
| 2375 | // `class_attr(&[..])` shape, and a name it cannot see is a name nobody |
| 2376 | // is checking against makeover's. |
| 2377 | match clamp |
| 2378 | Some => class_attr, |
| 2379 | None => class_attr, |
| 2380 | |
| 2381 | action_attrs; |
| 2382 | out.push; |
| 2383 | close |
| 2384 | else |
| 2385 | out.push_str; |
| 2386 | match clamp |
| 2387 | Some => class_attr, |
| 2388 | None => class_attr, |
| 2389 | |
| 2390 | out.push; |
| 2391 | "</span>" |
| 2392 | ; |
| 2393 | |
| 2394 | for cell in group |
| 2395 | for node in &cell.content |
| 2396 | row_inline_html; |
| 2397 | |
| 2398 | |
| 2399 | |
| 2400 | out.push_str; |
| 2401 | |
| 2402 | |
| 2403 | /// One leaf inside a row's run. |
| 2404 | /// |
| 2405 | /// Markdown is the one place a run entry is not just `node_html`. A |
| 2406 | /// [`Node::Rich`] standing on its own is a block and renders as one; inside a |
| 2407 | /// row it goes through docengine's `phrase` preset instead, which is markdown |
| 2408 | /// with no block structure at all and no links, keeping the inline emphasis. A |
| 2409 | /// heading, a list and a quote each contribute their words without claiming a |
| 2410 | /// block of a row that has no room for one, and not even a paragraph survives. |
| 2411 | /// |
| 2412 | /// Links go because a row usually carries [`Row::activate`], so the row itself |
| 2413 | /// is already a target and an anchor inside it is a second target inside the |
| 2414 | /// first: ambiguous to click, worse to reach by keyboard, and pointing |
| 2415 | /// somewhere a one-line summary cannot usefully send anyone. Their text stays. |
| 2416 | /// |
| 2417 | /// This is the renderer deciding, which is the point of the description |
| 2418 | /// carrying the kind rather than a flattened string. A terminal renderer facing |
| 2419 | /// the same source can emit bold instead, and one that wants neither can call |
| 2420 | /// `docengine::render_plain`. None of them has to be told by the screen author |
| 2421 | /// which to do. |
| 2422 | |
| 2423 | match node |
| 2424 | Text => escape_into, |
| 2425 | // The inline path takes neither axis, and that is a containment |
| 2426 | // decision rather than an oversight: a run inside a cell is one line in |
| 2427 | // something usually already clickable, so blocks and links go whatever |
| 2428 | // the node declares. `Richness` says nothing about phrases for the same |
| 2429 | // reason -- see its doc. |
| 2430 | Rich => out.push_str, |
| 2431 | Token => tag_html, |
| 2432 | Act => act_html, |
| 2433 | Meter => meter_html_into, |
| 2434 | Chart => chart_html_into |
| 2435 | &axis.as_layout, |
| 2436 | bars.iter.map, |
| 2437 | opts, |
| 2438 | out, |
| 2439 | , |
| 2440 | // Every other leaf the model admits into a run. A link in a row and a |
| 2441 | // figure in a row were the two gaps the enumeration left open and could |
| 2442 | // not close without a `RowPart` variant each; here they arrive by |
| 2443 | // already being leaves. |
| 2444 | // |
| 2445 | // No fills: a bespoke region is a block and cannot reach a run, which |
| 2446 | // is what the containment bound guarantees. |
| 2447 | other => node_html, |
| 2448 | |
| 2449 | |
| 2450 | |
| 2451 | /// One cell's inline run. |
| 2452 | /// |
| 2453 | /// A part per inline rather than a part per cell, which is the half of the |
| 2454 | /// containment model this crate had to learn. Before it, a cell was four |
| 2455 | /// members and this function was a fixed sequence: the value or the link, then |
| 2456 | /// the tokens strip, then the actions strip. The run says the order itself, so |
| 2457 | /// a cell holding a tag between two words draws that way instead of hoisting |
| 2458 | /// the tag to the end. |
| 2459 | /// |
| 2460 | /// Consecutive tokens and consecutive acts still share one wrapping strip. |
| 2461 | /// `cell-tokens` and `cell-actions` carry the gap between siblings, so a span |
| 2462 | /// each would space them as though they were unrelated, and the common case -- |
| 2463 | /// a status column of three badges -- is exactly the consecutive one. |
| 2464 | /// |
| 2465 | /// Writes into a buffer the caller owns rather than answering with one. The |
| 2466 | /// caller is [`cells_row_html`], which needs every cell of a row to exist at |
| 2467 | /// once because `cells_html` takes them together -- but needs that only within |
| 2468 | /// the row, so the buffers are reused down the table and a fresh `String` per |
| 2469 | /// cell per row was the emitter's largest remaining cost. |
| 2470 | |
| 2471 | // A cell that is one piece of text says so on the container through |
| 2472 | // `CellPart::Value`, so a wrapper span here would say nothing the container |
| 2473 | // has not. Anything else names its parts inside, or the content colour on |
| 2474 | // the cell reaches the tokens and the controls beside the text -- the drift |
| 2475 | // makeover-layout 0.14.0 named and makeover-webview 0.25.0 stopped |
| 2476 | // emitting. |
| 2477 | if let = cell.content.as_slice |
| 2478 | escape_into; |
| 2479 | return; |
| 2480 | |
| 2481 | |
| 2482 | let mut rest = cell.content.as_slice; |
| 2483 | while let Some = rest.split_first |
| 2484 | match head |
| 2485 | Text => |
| 2486 | out.push_str; |
| 2487 | class_attr; |
| 2488 | out.push; |
| 2489 | escape_into; |
| 2490 | out.push_str; |
| 2491 | rest = tail; |
| 2492 | |
| 2493 | // The row is a `div` and not an anchor even when it activates, so |
| 2494 | // this nests nothing: the href lands on the row element as an |
| 2495 | // attribute htmx reads, and the only `<a>` in the row is the one a |
| 2496 | // cell asked for. |
| 2497 | // |
| 2498 | // `data-act` for the same reason a button carries it. The row's |
| 2499 | // `ClickBeside` filter keys on that attribute, so without it a click |
| 2500 | // on the title would follow the link and open the row underneath. |
| 2501 | Link => |
| 2502 | let = control_tag; |
| 2503 | out.push_str; |
| 2504 | class_attr; |
| 2505 | out.push_str; |
| 2506 | action_attrs; |
| 2507 | out.push; |
| 2508 | escape_into; |
| 2509 | out.push_str; |
| 2510 | rest = tail; |
| 2511 | |
| 2512 | Token => |
| 2513 | let run = rest.iter.take_while; |
| 2514 | out.push_str; |
| 2515 | class_attr; |
| 2516 | out.push; |
| 2517 | let mut taken = 0; |
| 2518 | for part in run |
| 2519 | if let Token = part |
| 2520 | tag_html; |
| 2521 | |
| 2522 | taken += 1; |
| 2523 | |
| 2524 | out.push_str; |
| 2525 | rest = &rest; |
| 2526 | |
| 2527 | // Deliberately not `row-actions`. That was a hover-reveal rule |
| 2528 | // until makeover-webview 0.23.0 retired it, and it is a list row's |
| 2529 | // class besides: `cell-actions` is the table's own, and it carries |
| 2530 | // no colour so a button here is not painted as text. |
| 2531 | Act => |
| 2532 | let run = rest.iter.take_while; |
| 2533 | out.push_str; |
| 2534 | class_attr; |
| 2535 | out.push; |
| 2536 | let mut taken = 0; |
| 2537 | for part in run |
| 2538 | if let Act = part |
| 2539 | act_html; |
| 2540 | |
| 2541 | taken += 1; |
| 2542 | |
| 2543 | out.push_str; |
| 2544 | rest = &rest; |
| 2545 | |
| 2546 | // Every other leaf the model now admits into a run. A meter and a |
| 2547 | // figure in a cell were the two gaps the enumeration left open and |
| 2548 | // could not close without a member each; here they arrive by |
| 2549 | // already being leaves. |
| 2550 | other => |
| 2551 | // No fills: a bespoke region is a block and cannot reach a run, |
| 2552 | // which the containment bound is what guarantees. |
| 2553 | node_html; |
| 2554 | rest = tail; |
| 2555 | |
| 2556 | |
| 2557 | |
| 2558 | |
| 2559 | |
| 2560 | /// The buffers a table's rows take turns in. |
| 2561 | /// |
| 2562 | /// `cells_html` takes a row's cells together, so the markup of every cell in one |
| 2563 | /// row has to exist at once. Nothing says it has to be new: cleared and |
| 2564 | /// refilled, these keep the capacity the first row bought, and a table's second |
| 2565 | /// row onwards writes into memory that already exists. |
| 2566 | /// |
| 2567 | /// [`Emitted`] is not here, because it borrows from `filled` and a struct |
| 2568 | /// holding both would be self-referential. It is a `Vec` per row and stays one. |
| 2569 | |
| 2570 | |
| 2571 | /// One cell's markup each, in column order. |
| 2572 | filled: , |
| 2573 | /// Which cells are a single piece of text, which is what earns the |
| 2574 | /// container the part class. |
| 2575 | parts: , |
| 2576 | |
| 2577 | |
| 2578 | /// One row of a table. |
| 2579 | /// |
| 2580 | /// Takes the columns already borrowed rather than the described ones. They are |
| 2581 | /// the same for every row of the table, and this built the borrowed list again |
| 2582 | /// per row until the emitter's allocations were counted. |
| 2583 | |
| 2584 | cells: &Row, |
| 2585 | columns: &[], |
| 2586 | folded: bool, |
| 2587 | buffers: &mut RowBuffers, |
| 2588 | opts: &Emit, |
| 2589 | out: &mut String, |
| 2590 | |
| 2591 | let classes = row_classes; |
| 2592 | out.push_str; |
| 2593 | class_attr; |
| 2594 | row_attrs_html; |
| 2595 | // The activate route, which only a table row carries in its opening tag: a |
| 2596 | // list row's is written by its run instead. Kept here rather than moved |
| 2597 | // into the shared attrs because the two really do differ. |
| 2598 | if let Some = &cells.activate |
| 2599 | let carries_control = cells.cells.iter.any; |
| 2600 | let fires = if carries_control || cells.selected.is_some |
| 2601 | ClickBeside |
| 2602 | else |
| 2603 | Click |
| 2604 | ; |
| 2605 | action_attrs; |
| 2606 | |
| 2607 | out.push; |
| 2608 | |
| 2609 | // The tick, in a cell of its own before the columns. A list row draws this |
| 2610 | // in its gutter and a table has no gutter, so the gutter is a cell -- the |
| 2611 | // head emits a matching one, because `display: table` aligns by position. |
| 2612 | // |
| 2613 | // The same `row-select` class a list row's tick carries, and deliberately: |
| 2614 | // it is the selector a commit control gathers by, so a screen mixing a list |
| 2615 | // and a table has one selection rather than two that look alike. A cell |
| 2616 | // class as well, for the width, since this one is not a column and takes no |
| 2617 | // `col-` class. |
| 2618 | // The disclosure, in a cell of its own before the tick, on the grounds the |
| 2619 | // tick takes its own: it is not a value in the grid, it does not narrow |
| 2620 | // with the columns and it does not sort. The head emits a matching cell, |
| 2621 | // because `display: table` aligns by position. |
| 2622 | if let Some = cells.open |
| 2623 | out.push_str; |
| 2624 | class_attr; |
| 2625 | out.push_str; |
| 2626 | class_attr; |
| 2627 | out.push_str; |
| 2628 | out.push_str; |
| 2629 | out.push_str; |
| 2630 | out.push_str; |
| 2631 | out.push_str; |
| 2632 | |
| 2633 | |
| 2634 | if let Some = cells.selected |
| 2635 | // A `div` with no role, matching the cells makeover emits beside it. |
| 2636 | // The row says `role="row"` and its cells say nothing, which is |
| 2637 | // makeover's shape; a lone `role="cell"` here would be the one cell in |
| 2638 | // the table announcing itself differently from its neighbours. |
| 2639 | out.push_str; |
| 2640 | class_attr; |
| 2641 | out.push_str; |
| 2642 | class_attr; |
| 2643 | if ticked |
| 2644 | out.push_str; |
| 2645 | |
| 2646 | if let Some = &cells.value |
| 2647 | out.push_str; |
| 2648 | escape_into; |
| 2649 | out.push_str; |
| 2650 | escape_into; |
| 2651 | out.push; |
| 2652 | |
| 2653 | // No `toggle` counterpart, unlike a list row. A table row's tick is a |
| 2654 | // member of a set by construction (`Row::ticking` is the only way to |
| 2655 | // set it), where a list row's may be the write itself. |
| 2656 | out.push_str; |
| 2657 | |
| 2658 | |
| 2659 | // The cell contents are escaped here and handed over as Markup, which is |
| 2660 | // makeover-webview's contract: it owns the structure, the caller owns what |
| 2661 | // goes in. Ours is text from a description plus, since `022f0c59`, whatever |
| 2662 | // controls the cell carries, and `cells_html` is what knows the column |
| 2663 | // classes and the narrowing. |
| 2664 | let RowBuffers = buffers; |
| 2665 | filled.truncate; |
| 2666 | for in cells.cells.iter.enumerate |
| 2667 | match filled.get_mut |
| 2668 | Some => buffer.clear, |
| 2669 | None => filled.push, |
| 2670 | |
| 2671 | cell_run_html; |
| 2672 | |
| 2673 | // The container says what the cell is only when the cell is nothing but one |
| 2674 | // piece of text, which is the case where a wrapper span would say nothing |
| 2675 | // the container has not already said. Anything else names its parts inside |
| 2676 | // -- the anchor is a `cell-link`, the strips are `cell-tokens` and |
| 2677 | // `cell-actions` -- because a colour on the container would reach all of |
| 2678 | // them, and that is the drift makeover-layout 0.14.0 named. |
| 2679 | parts.clear; |
| 2680 | parts.extend |
| 2681 | matches!.then_some |
| 2682 | ; |
| 2683 | let emitted: = columns |
| 2684 | .iter |
| 2685 | .zip |
| 2686 | .zip |
| 2687 | .map| Emitted |
| 2688 | column: column.name, |
| 2689 | part, |
| 2690 | content: Markup, |
| 2691 | ) |
| 2692 | .collect; |
| 2693 | let mut marked = crateopen; |
| 2694 | if marked.watching |
| 2695 | // Told where each block went rather than looking for it. A cell that |
| 2696 | // renders to nothing cannot be searched for, and a cell whose markup |
| 2697 | // repeats in the same row would be found twice. |
| 2698 | let mut placed = Vecnew; |
| 2699 | cells_html_placed; |
| 2700 | for block in &placed |
| 2701 | marked.wrote; |
| 2702 | |
| 2703 | else |
| 2704 | cells_html_into; |
| 2705 | |
| 2706 | |
| 2707 | // After the cells, and outside the grid: a menu is not a value in the row, |
| 2708 | // so it takes no column and the head emits nothing to match it. That is the |
| 2709 | // one way it differs from the tick above, which is a cell because it is |
| 2710 | // drawn in line with them. |
| 2711 | // |
| 2712 | // `data-menu="row"` is the list row's own attribute, deliberately: it is what |
| 2713 | // the host's menu code binds to, and a second name for "this row's menu" |
| 2714 | // would make a screen mixing a list and a table need two bindings for one |
| 2715 | // gesture. The class is `table-row-menu`, matching how `table-row-current` |
| 2716 | // shadows `row-current` -- that half is styling, and a table row's menu is |
| 2717 | // positioned against a grid rather than against a line. |
| 2718 | // |
| 2719 | // `hidden` keeps it out of the accessibility tree, which is also what makes |
| 2720 | // it legal here: a `role="row"` whose visible children are all cells stays |
| 2721 | // well-formed, and a `display: none` child takes no track in either a table |
| 2722 | // or a grid, so the columns above line up unchanged. |
| 2723 | if !cells.menu.is_empty |
| 2724 | out.push_str; |
| 2725 | class_attr; |
| 2726 | out.push_str; |
| 2727 | for act in &cells.menu |
| 2728 | marked.starts; |
| 2729 | act_html; |
| 2730 | marked.ends; |
| 2731 | |
| 2732 | out.push_str; |
| 2733 | |
| 2734 | |
| 2735 | marked.close; |
| 2736 | out.push_str; |
| 2737 | |
| 2738 | |
| 2739 | /// The class saying when a region member drops. |
| 2740 | /// |
| 2741 | /// `makeover-webview`'s table half already maps a [`layout::Priority`] onto |
| 2742 | /// one of these, and the stylesheet already hides them at the right size |
| 2743 | /// classes: `cell-drops-first` at `Medium` and below, `cell-drops-next` at |
| 2744 | /// `Compact`. Both rules are written against the bare class rather than |
| 2745 | /// against a table, so a region member wearing one narrows with the tables |
| 2746 | /// beside it and by the same declared rule. |
| 2747 | /// |
| 2748 | /// The names say "cell" because that is where they were needed first. Reusing |
| 2749 | /// them is deliberate: a second set of names would be a second encoding of one |
| 2750 | /// fact, and the two would have to be kept in agreement by hand. Renaming the |
| 2751 | /// pair to something placement-neutral belongs in `makeover-webview`, and the |
| 2752 | /// mapping wants to be public there rather than copied here -- it is private |
| 2753 | /// today, which is the only reason this function exists. |
| 2754 | |
| 2755 | match priority |
| 2756 | Optional => Some, |
| 2757 | Secondary => Some, |
| 2758 | // A tier added upstream keeps its place. Of the two ways to be wrong |
| 2759 | // about a priority this renderer has not learned, showing something |
| 2760 | // that should have dropped is the one the reader can see and work |
| 2761 | // around. |
| 2762 | _ => None, |
| 2763 | |
| 2764 | |
| 2765 | |
| 2766 | /// One member of a region, and what it is worth when room runs out. |
| 2767 | /// |
| 2768 | /// The wrapper appears only for a member that can drop, so every region |
| 2769 | /// written before [`quasi_router::Ranked`] existed -- which is all of them, |
| 2770 | /// since [`Slot::with`] still ranks at `Essential` -- emits exactly the markup |
| 2771 | /// it always did. |
| 2772 | |
| 2773 | let drops = drop_class; |
| 2774 | let divides = share_attr.is_some; |
| 2775 | if drops.is_none && !divides |
| 2776 | node_html; |
| 2777 | return; |
| 2778 | |
| 2779 | out.push_str; |
| 2780 | if let Some = drops |
| 2781 | class_attr; |
| 2782 | |
| 2783 | if let Some = share_attr |
| 2784 | out.push_str; |
| 2785 | out.push_str; |
| 2786 | out.push; |
| 2787 | |
| 2788 | out.push; |
| 2789 | node_html; |
| 2790 | out.push_str; |
| 2791 | |
| 2792 | |
| 2793 | /// How much of its row a member asked for, where it is not the default. |
| 2794 | /// |
| 2795 | /// `width_attr`'s attribute and the opposite default, which is the whole |
| 2796 | /// difference between the two positions. A control's width defaults to |
| 2797 | /// [`layout::Width::Fill`] because that is what a control did before the member |
| 2798 | /// existed; a run member's defaults to [`layout::Width::Content`] because a |
| 2799 | /// flex item with `min-width: min-content` and no grow is content-sized. So |
| 2800 | /// each side omits the value that changes nothing, and they omit opposite ones. |
| 2801 | /// |
| 2802 | /// [`layout::Width::Fixed`] is stated and not implemented. A run carries no |
| 2803 | /// size by design, so there is nothing to fix a member at, and the honest |
| 2804 | /// answer is to carry the description's word into the markup and let the |
| 2805 | /// stylesheet say nothing about it yet. |
| 2806 | |
| 2807 | match width |
| 2808 | Content => None, |
| 2809 | Fixed => Some, |
| 2810 | Fill => Some, |
| 2811 | // `Width` is `#[non_exhaustive]`; a member added upstream is one this |
| 2812 | // renderer has not learned, and drawing it as content is what it drew |
| 2813 | // before the member existed. |
| 2814 | _ => None, |
| 2815 | |
| 2816 | |
| 2817 | |
| 2818 | /// One thing on a screen. |
| 2819 | pub |
| 2820 | match node |
| 2821 | Heading => |
| 2822 | let tag = match level |
| 2823 | Page => "h1", |
| 2824 | Section => "h2", |
| 2825 | Subsection => "h3", |
| 2826 | ; |
| 2827 | out.push; |
| 2828 | out.push_str; |
| 2829 | class_attr; |
| 2830 | out.push; |
| 2831 | escape_into; |
| 2832 | out.push_str; |
| 2833 | out.push_str; |
| 2834 | out.push; |
| 2835 | |
| 2836 | |
| 2837 | Text => |
| 2838 | out.push_str; |
| 2839 | class_attr; |
| 2840 | tone_attr; |
| 2841 | out.push; |
| 2842 | escape_into; |
| 2843 | out.push_str; |
| 2844 | |
| 2845 | |
| 2846 | // `19d7602d`. The runs are already classified, so this writes a span |
| 2847 | // per run carrying the class name `layout::Syntax` spells and nothing |
| 2848 | // else: no highlighter enters this crate, and a host's stylesheet says |
| 2849 | // what each class looks like. `docengine::Emphasis` crosses the seam |
| 2850 | // the same way. |
| 2851 | // |
| 2852 | // A run at `Syntax::Plain` gets no span at all. It is most of a file, |
| 2853 | // its class would say only "ordinary", and a span per word triples the |
| 2854 | // document for nothing. |
| 2855 | Code |
| 2856 | runs, |
| 2857 | language, |
| 2858 | inline, |
| 2859 | => |
| 2860 | let = if *inline |
| 2861 | |
| 2862 | else |
| 2863 | |
| 2864 | ; |
| 2865 | out.push_str; |
| 2866 | class_attr; |
| 2867 | // The hint, as an attribute rather than a class: a language is app |
| 2868 | // text and a class built from it would be a class name built from |
| 2869 | // arbitrary input. A host keying off it has `[data-language]`. |
| 2870 | if let Some = language |
| 2871 | out.push_str; |
| 2872 | escape_into; |
| 2873 | out.push; |
| 2874 | |
| 2875 | out.push; |
| 2876 | for run in runs |
| 2877 | if run.syntax == Plain |
| 2878 | escape_into; |
| 2879 | continue; |
| 2880 | |
| 2881 | out.push_str; |
| 2882 | // Not `class_attr`: that prefixes with the emitter's own |
| 2883 | // namespace, and these are one class each from a closed set the |
| 2884 | // vocabulary spells. `lex-` rather than a bare `keyword`, |
| 2885 | // because `.string` and `.comment` are names a host stylesheet |
| 2886 | // will already have used for something else. |
| 2887 | out.push_str; |
| 2888 | out.push_str; |
| 2889 | out.push_str; |
| 2890 | escape_into; |
| 2891 | out.push_str; |
| 2892 | |
| 2893 | out.push_str; |
| 2894 | |
| 2895 | |
| 2896 | StandIn |
| 2897 | state, |
| 2898 | message, |
| 2899 | act, |
| 2900 | marks, |
| 2901 | => |
| 2902 | // The markup is makeover-webview's, unchanged, for the reason every |
| 2903 | // other emitter here defers to it: the CSS that has to match it is |
| 2904 | // emitted there too. The way out arrives as `Markup` because a |
| 2905 | // button is an address and no crate down there names one. |
| 2906 | let mut way_out = Stringnew; |
| 2907 | if let Some = act |
| 2908 | act_html; |
| 2909 | |
| 2910 | let action = .then_some; |
| 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 = crateopen; |
| 2916 | if marked.watching |
| 2917 | let mut placed = None; |
| 2918 | placeholder_html_placed |
| 2919 | *state, |
| 2920 | message, |
| 2921 | action, |
| 2922 | opts, |
| 2923 | out, |
| 2924 | &mut placed, |
| 2925 | ; |
| 2926 | if let Some = placed |
| 2927 | marked.wrote; |
| 2928 | |
| 2929 | marked.close; |
| 2930 | else |
| 2931 | placeholder_html_into; |
| 2932 | |
| 2933 | |
| 2934 | |
| 2935 | Rich |
| 2936 | source, |
| 2937 | richness, |
| 2938 | trust, |
| 2939 | => |
| 2940 | out.push_str; |
| 2941 | class_attr; |
| 2942 | // The fence hyperscript owes, wherever text a user typed becomes |
| 2943 | // markup. `0081563e`: a loaded interpreter reads `_` attributes |
| 2944 | // anywhere in the document, creator-authored content included, and |
| 2945 | // this is the one node whose content is authored by somebody other |
| 2946 | // than the app. |
| 2947 | // |
| 2948 | // Belt and braces rather than the only guard, and it earns its |
| 2949 | // place now that `Trust::Trusted` exists: an untrusted source has |
| 2950 | // its raw HTML stripped and never reaches this fence with an |
| 2951 | // attribute in it, but a trusted one keeps its markup, so the fence |
| 2952 | // is what stops the app's own copy from carrying a `_` attribute |
| 2953 | // into a document with a loaded interpreter. The guarantee no |
| 2954 | // longer depends on which way the trust axis is set. |
| 2955 | out.push_str; |
| 2956 | out.push; |
| 2957 | out.push_str; |
| 2958 | out.push_str; |
| 2959 | |
| 2960 | |
| 2961 | Act => act_html, |
| 2962 | |
| 2963 | // Text that goes somewhere. `control_tag` picks the anchor or the |
| 2964 | // button from the method, the same way a cell link already did, and |
| 2965 | // `link` is the class makeover names it with. |
| 2966 | Link => |
| 2967 | let = control_tag; |
| 2968 | out.push_str; |
| 2969 | class_attr; |
| 2970 | out.push_str; |
| 2971 | action_attrs; |
| 2972 | out.push; |
| 2973 | escape_into; |
| 2974 | out.push_str; |
| 2975 | |
| 2976 | |
| 2977 | Token => tag_html, |
| 2978 | |
| 2979 | // The readouts derived from the current time. The first words are made |
| 2980 | // here and every set after them by `clock.js`, which reads the two |
| 2981 | // attributes this writes; see `crate::clock` for why the format is |
| 2982 | // stated twice and `Shell::clock_src` for what a document without the |
| 2983 | // script gets. |
| 2984 | Since => clock_html, |
| 2985 | Until => clock_html, |
| 2986 | Age => clock_html, |
| 2987 | |
| 2988 | // One figure, through the same emitter the strip uses. What differs is |
| 2989 | // that nothing wraps it: the run it sits in is already the grouping. |
| 2990 | Figure => |
| 2991 | figure_html_into; |
| 2992 | |
| 2993 | |
| 2994 | Image => |
| 2995 | // A captioned picture is a `<figure>`, which is what the element is |
| 2996 | // for and what the shipped carousel already writes by hand. An |
| 2997 | // uncaptioned one is the bare `<img>`: wrapping it would put a |
| 2998 | // grouping element around a group of one. |
| 2999 | let captioned = picture.caption.is_some; |
| 3000 | if captioned |
| 3001 | out.push_str; |
| 3002 | class_attr; |
| 3003 | out.push; |
| 3004 | |
| 3005 | |
| 3006 | img_tag; |
| 3007 | |
| 3008 | if let Some = &picture.caption |
| 3009 | out.push_str; |
| 3010 | class_attr; |
| 3011 | out.push; |
| 3012 | escape_into; |
| 3013 | out.push_str; |
| 3014 | |
| 3015 | |
| 3016 | |
| 3017 | Notice |
| 3018 | kind, |
| 3019 | tone, |
| 3020 | text, |
| 3021 | act, |
| 3022 | => |
| 3023 | out.push_str; |
| 3024 | class_attr |
| 3025 | & |
| 3026 | Toast => "toast", |
| 3027 | Banner => "banner", |
| 3028 | ], |
| 3029 | opts, |
| 3030 | out, |
| 3031 | ; |
| 3032 | tone_attr; |
| 3033 | // The hook the clock script takes a toast away by. An attribute |
| 3034 | // rather than the class above, for `data-clock`'s reason: a class |
| 3035 | // is prefixed by host configuration and a script shipped by this |
| 3036 | // crate cannot know the prefix. `4453bf82`. |
| 3037 | // |
| 3038 | // Emitted for the transient kind only, because it means "this one |
| 3039 | // is on a clock" rather than "this one is a notice". A banner has |
| 3040 | // no deadline and nothing to say to the script. |
| 3041 | if kind.transient |
| 3042 | out.push_str; |
| 3043 | |
| 3044 | // A danger or warning notice interrupts; anything else waits for a |
| 3045 | // pause. The description already says which through its tone, so |
| 3046 | // the renderer does not need a second field to be told. |
| 3047 | let assertive = matches!; |
| 3048 | if assertive |
| 3049 | out.push_str; |
| 3050 | else |
| 3051 | out.push_str; |
| 3052 | |
| 3053 | out.push; |
| 3054 | escape_into; |
| 3055 | // The one thing to do about it, inside the notice rather than |
| 3056 | // beside it: a control that scrolled away from the sentence |
| 3057 | // explaining it is a button with no subject. `bde35298`. |
| 3058 | if let Some = act |
| 3059 | act_html; |
| 3060 | |
| 3061 | out.push_str; |
| 3062 | |
| 3063 | |
| 3064 | Field => field_group_html, |
| 3065 | |
| 3066 | Form |
| 3067 | action, |
| 3068 | submit, |
| 3069 | fields, |
| 3070 | marks, |
| 3071 | => |
| 3072 | out.push_str; |
| 3073 | class_attr; |
| 3074 | action_attrs; |
| 3075 | out.push; |
| 3076 | let mut marked = crateopen; |
| 3077 | for field in fields |
| 3078 | marked.starts; |
| 3079 | field_group_html; |
| 3080 | marked.ends; |
| 3081 | |
| 3082 | marked.close; |
| 3083 | out.push_str; |
| 3084 | class_attr; |
| 3085 | out.push; |
| 3086 | escape_into; |
| 3087 | out.push_str; |
| 3088 | |
| 3089 | |
| 3090 | Timeline |
| 3091 | track, |
| 3092 | entries, |
| 3093 | focus, |
| 3094 | .. |
| 3095 | => |
| 3096 | // The axis first, then the things on it. Two children of one |
| 3097 | // positioned box, so the entries resolve their percentages against |
| 3098 | // the same height the slots fill. |
| 3099 | out.push_str; |
| 3100 | class_attr; |
| 3101 | // The focus travels as data rather than as a scroll offset: the app |
| 3102 | // knows the interesting hour, the host decides how to get there. |
| 3103 | // goingson's JS hardcodes `targetHour = 9` inside its renderer, |
| 3104 | // which is the arrangement this replaces. |
| 3105 | if let Some = focus |
| 3106 | let _ = write!; |
| 3107 | |
| 3108 | out.push; |
| 3109 | |
| 3110 | // The ruler. Slots are the grid the eye reads against; a tick every |
| 3111 | // `tick` minutes carries the label. Both are the axis describing |
| 3112 | // itself, so neither is an entry and neither is addressable. |
| 3113 | let slots = track.slots; |
| 3114 | let tick_every = if track.slot == 0 || track.tick == 0 |
| 3115 | 0 |
| 3116 | else |
| 3117 | track.tick / track.slot |
| 3118 | ; |
| 3119 | for slot in 0..slots |
| 3120 | out.push_str; |
| 3121 | class_attr; |
| 3122 | out.push; |
| 3123 | if tick_every > 0 && slot % tick_every == 0 |
| 3124 | let minute = track.span.from + slot * track.slot; |
| 3125 | out.push_str; |
| 3126 | class_attr; |
| 3127 | out.push; |
| 3128 | // The label the axis's unit asks for. This assumed minutes |
| 3129 | // until 2026-08-15 and printed `00:00` over a month strip, |
| 3130 | // which is the defect `layout::Unit` exists to close: the |
| 3131 | // geometry above is unit-agnostic and correct either way, |
| 3132 | // so nothing else here noticed. |
| 3133 | match track.unit |
| 3134 | // Wall clock, wrapped, so a span running past midnight |
| 3135 | // labels 02:00 rather than 26:00. The wrap is |
| 3136 | // presentation: `Span` deliberately counts past 1440 so |
| 3137 | // it needs no date, and how that reads to a person is |
| 3138 | // this renderer's call. |
| 3139 | Minutes => |
| 3140 | let _ = write!; |
| 3141 | |
| 3142 | // Day one, not day zero. A strip's offsets are |
| 3143 | // zero-based like every other axis here, and nobody |
| 3144 | // calls the first of the month the zeroth. |
| 3145 | Days => |
| 3146 | let _ = write!; |
| 3147 | |
| 3148 | // A unit added later lands here rather than silently |
| 3149 | // taking the clock, which is exactly how this bug was |
| 3150 | // shipped in the first place. |
| 3151 | _ => |
| 3152 | |
| 3153 | out.push_str; |
| 3154 | |
| 3155 | out.push_str; |
| 3156 | |
| 3157 | |
| 3158 | // Lanes. Overlapping entries sit side by side, and which lane each |
| 3159 | // takes is worked out here rather than described, because it is a |
| 3160 | // fact about how wide the box is and not about the day. The |
| 3161 | // description said when things happen; `Placement::overlaps` turns |
| 3162 | // that into who collides. |
| 3163 | // |
| 3164 | // Greedy first-fit against the entries already placed, which is the |
| 3165 | // standard day-view packing: an entry takes the lowest lane no |
| 3166 | // occupant of which it overlaps. O(n^2) in the worst case and n is a |
| 3167 | // day's worth of appointments, so the clever interval graph is not |
| 3168 | // worth its own bugs here. |
| 3169 | let mut lanes: = Vecwith_capacity; |
| 3170 | for in entries.iter.enumerate |
| 3171 | let mut lane = 0; |
| 3172 | loop |
| 3173 | let taken = entries |
| 3174 | .iter |
| 3175 | .zip |
| 3176 | .any; |
| 3177 | if !taken |
| 3178 | break; |
| 3179 | |
| 3180 | lane += 1; |
| 3181 | |
| 3182 | lanes.push; |
| 3183 | |
| 3184 | // One width for the whole track rather than per collision cluster. |
| 3185 | // Per-cluster is denser and is a layout decision this renderer can |
| 3186 | // revisit without the description changing, which is the point of |
| 3187 | // it being here. |
| 3188 | let width = lanes.iter.copied.max.map_or; |
| 3189 | |
| 3190 | for in entries.iter.zip |
| 3191 | let at = track.fraction; |
| 3192 | let end = track.fraction; |
| 3193 | out.push_str; |
| 3194 | class_attr; |
| 3195 | // The only inline style this crate writes, and it carries no |
| 3196 | // colour, no size and no opinion: four numbers makeover's rules |
| 3197 | // read. A stylesheet cannot hold these, because they are the |
| 3198 | // data. |
| 3199 | let _ = write! |
| 3200 | out, |
| 3201 | " style=\"--track-at:{:.4}%;--track-for:{:.4}%;--track-lane:{lane};--track-lanes:{width}\"" |
| 3202 | at * 100.0, |
| 3203 | * 100.0 |
| 3204 | ); |
| 3205 | out.push; |
| 3206 | row_html; |
| 3207 | out.push_str; |
| 3208 | |
| 3209 | |
| 3210 | out.push_str; |
| 3211 | |
| 3212 | |
| 3213 | Table |
| 3214 | columns, |
| 3215 | rows, |
| 3216 | more, |
| 3217 | marks, |
| 3218 | => |
| 3219 | // **A table that declared no columns is a list, and draws as one.** |
| 3220 | // One node since the 2026-09-06 collapse, and this is the whole of |
| 3221 | // where the two arrangements part company in this renderer. |
| 3222 | // |
| 3223 | // `<ul>` rather than `role="table"` over a feed, deliberately. The |
| 3224 | // model is unified and the presentation is not: a feed of items is a |
| 3225 | // list to a screen reader, and announcing it as a table with one |
| 3226 | // column would be a worse document rather than a more consistent |
| 3227 | // one. The rows are the same `Row` either way, which is the point -- |
| 3228 | // nothing about what the description *says* changes here, only how |
| 3229 | // it is arranged. |
| 3230 | if columns.is_empty |
| 3231 | out.push_str; |
| 3232 | class_attr; |
| 3233 | out.push; |
| 3234 | // Which rows a shut branch folds away, asked once for the list |
| 3235 | // rather than per row: it is a fact about the run of rows and a |
| 3236 | // row cannot see the ones above it. Every row is emitted either |
| 3237 | // way and the folded ones are `hidden`, so opening a branch is a |
| 3238 | // class change in the browser rather than a round trip for rows |
| 3239 | // the document already holds. |
| 3240 | let folded = folded; |
| 3241 | // A list declared no columns, so its index space is the rows |
| 3242 | // and then the pager. `Table::placed` counts the columns first |
| 3243 | // and there are none, so the two agree without a special case. |
| 3244 | let mut marked = crateopen; |
| 3245 | for in rows.iter.zip |
| 3246 | marked.starts; |
| 3247 | row_html; |
| 3248 | marked.ends; |
| 3249 | |
| 3250 | out.push_str; |
| 3251 | |
| 3252 | // Outside the list, because it is not one of the things in it. |
| 3253 | if let Some = more |
| 3254 | marked.starts; |
| 3255 | rest_html; |
| 3256 | marked.ends; |
| 3257 | |
| 3258 | marked.close; |
| 3259 | return; |
| 3260 | |
| 3261 | |
| 3262 | let borrowed: = columns |
| 3263 | .iter |
| 3264 | .map |
| 3265 | .collect; |
| 3266 | // No CSS travels with the table, and none can. A described table's |
| 3267 | // columns are known here rather than at build time, so a track list |
| 3268 | // would have to be emitted per table: a `<style>` element beside it, |
| 3269 | // which needs `style-src 'unsafe-inline'`, or a head block, which a |
| 3270 | // fragment swap does not carry. makeover's stylesheet lays the table |
| 3271 | // out with `display: table` instead, aligning columns across rows |
| 3272 | // knowing nothing about how many there are, and hides a dropped |
| 3273 | // column by the drop class its cells carry. The markup owes the |
| 3274 | // cells and headings those classes and nothing more. |
| 3275 | out.push_str; |
| 3276 | class_attr; |
| 3277 | out.push; |
| 3278 | |
| 3279 | // A tick takes no column, so the header owes it a cell that names |
| 3280 | // none. `display: table` aligns by position, so the gutter has to |
| 3281 | // exist in the head or every heading sits one place left of the |
| 3282 | // values under it. Empty rather than holding a select-all box: the |
| 3283 | // description says which rows exist and this crate emits no script |
| 3284 | // of its own, so ticking them all is a route the app offers (the |
| 3285 | // task list's `ticked=all`) rather than something a renderer can |
| 3286 | // invent. |
| 3287 | let ticks = rows.iter.any; |
| 3288 | // The disclosure gutter, on the same terms: a cell in the head or |
| 3289 | // every heading sits one place left of the values under it. |
| 3290 | let branches = rows.iter.any; |
| 3291 | |
| 3292 | out.push_str; |
| 3293 | class_attr; |
| 3294 | out.push; |
| 3295 | if branches |
| 3296 | out.push_str; |
| 3297 | class_attr; |
| 3298 | out.push_str; |
| 3299 | |
| 3300 | if ticks |
| 3301 | // `table-heading` as well, so it takes the head row's own |
| 3302 | // treatment and the gutter does not read as a gap. Labelled |
| 3303 | // rather than captioned: there is nothing to write above a |
| 3304 | // column of checkboxes that is not noise for everyone who can |
| 3305 | // see them, and a bare `columnheader` announces as an empty |
| 3306 | // heading to everyone who cannot. |
| 3307 | out.push_str; |
| 3308 | class_attr; |
| 3309 | out.push_str; |
| 3310 | |
| 3311 | let mut marked = crateopen; |
| 3312 | for in borrowed.iter.zip |
| 3313 | marked.starts; |
| 3314 | out.push_str; |
| 3315 | // The heading carries the same classes its column's cells do, |
| 3316 | // or the header and the body disagree about which column just |
| 3317 | // dropped and every heading below the cut sits over the wrong |
| 3318 | // values. `column_classes` is makeover's, so the two lists |
| 3319 | // cannot be assembled differently in two places. |
| 3320 | out.push_str; |
| 3321 | class_into; |
| 3322 | out.push; |
| 3323 | // Not escaped, unlike every other app-supplied string here. |
| 3324 | // makeover reduces a column name to identifier characters |
| 3325 | // rather than escaping it (0.41.0), because the same name is |
| 3326 | // written into a CSS selector by `narrowing_css` and an escaped |
| 3327 | // one would be safe in the attribute and unmatchable from the |
| 3328 | // stylesheet. Escaping the result again would encode nothing |
| 3329 | // and is the one way this heading could stop matching the cells |
| 3330 | // below it. |
| 3331 | push_column_classes; |
| 3332 | out.push; |
| 3333 | // `aria-sort` is what a table actually says about its order; |
| 3334 | // the caret in `state_rules` is this renderer's expression of |
| 3335 | // the same fact for everyone not using a screen reader. |
| 3336 | if let Some = column.sorted |
| 3337 | out.push_str; |
| 3338 | out.push_str; |
| 3339 | out.push; |
| 3340 | |
| 3341 | if column.sortable |
| 3342 | out.push_str; |
| 3343 | |
| 3344 | out.push; |
| 3345 | match &described.reorder |
| 3346 | // A button inside the header cell rather than attributes on |
| 3347 | // the cell itself: `role="columnheader"` is not a control, |
| 3348 | // and a screen reader offered a press on something that |
| 3349 | // announces itself as a heading has been lied to. |
| 3350 | Some => |
| 3351 | let = control_tag; |
| 3352 | out.push_str; |
| 3353 | class_attr; |
| 3354 | action_attrs; |
| 3355 | out.push; |
| 3356 | escape_into; |
| 3357 | out.push_str; |
| 3358 | |
| 3359 | None => escape_into, |
| 3360 | |
| 3361 | out.push_str; |
| 3362 | marked.ends; |
| 3363 | |
| 3364 | out.push_str; |
| 3365 | |
| 3366 | let mut buffers = default; |
| 3367 | // The fold, asked once for the table rather than per row: it is a |
| 3368 | // fact about the run of rows and a row cannot see the ones above it. |
| 3369 | let folds = folded; |
| 3370 | for in rows.iter.zip |
| 3371 | marked.starts; |
| 3372 | cells_row_html; |
| 3373 | marked.ends; |
| 3374 | |
| 3375 | out.push_str; |
| 3376 | |
| 3377 | // After the table's own element and not inside it, the way a list's |
| 3378 | // pager sits outside the `<ul>`: a pager is not a row, and a |
| 3379 | // `role="table"` with a non-row child is a table that says something |
| 3380 | // untrue about its own shape. |
| 3381 | if let Some = more |
| 3382 | marked.starts; |
| 3383 | rest_html; |
| 3384 | marked.ends; |
| 3385 | |
| 3386 | marked.close; |
| 3387 | |
| 3388 | |
| 3389 | // The trough and its tones are makeover-webview's, unchanged, for the |
| 3390 | // same reason the field markup is: a second emitter here would be the |
| 3391 | // same anatomy with a different escaping story, and the CSS it has to |
| 3392 | // match is emitted there too. |
| 3393 | Meter => meter_html_into, |
| 3394 | Chart => |
| 3395 | // Told where each bar went rather than looking for one. A bar's |
| 3396 | // markup is a function of its magnitude, so two bars of the same |
| 3397 | // height are the same bytes and searching would find either. |
| 3398 | let mut marked = crateopen; |
| 3399 | if marked.watching |
| 3400 | let mut placed = Vecnew; |
| 3401 | chart_html_placed |
| 3402 | &axis.as_layout, |
| 3403 | bars.iter.map, |
| 3404 | opts, |
| 3405 | out, |
| 3406 | &mut placed, |
| 3407 | ; |
| 3408 | for block in &placed |
| 3409 | marked.wrote; |
| 3410 | |
| 3411 | marked.close; |
| 3412 | else |
| 3413 | chart_html_into |
| 3414 | &axis.as_layout, |
| 3415 | bars.iter.map, |
| 3416 | opts, |
| 3417 | out, |
| 3418 | ; |
| 3419 | |
| 3420 | |
| 3421 | |
| 3422 | // The strip and its tones are makeover-webview's too, and the actions |
| 3423 | // are this crate's: a figure that answers a click becomes a control |
| 3424 | // wrapped round the emitted markup rather than a second figure emitter |
| 3425 | // that knows about routes. |
| 3426 | Stats => |
| 3427 | out.push_str; |
| 3428 | class_attr; |
| 3429 | out.push; |
| 3430 | let mut marked = crateopen; |
| 3431 | for in figures |
| 3432 | marked.starts; |
| 3433 | match action |
| 3434 | Some => |
| 3435 | let = control_tag; |
| 3436 | out.push_str; |
| 3437 | class_attr; |
| 3438 | action_attrs; |
| 3439 | out.push; |
| 3440 | figure_html_into; |
| 3441 | out.push_str; |
| 3442 | |
| 3443 | None => figure_html_into, |
| 3444 | |
| 3445 | marked.ends; |
| 3446 | |
| 3447 | marked.close; |
| 3448 | out.push_str; |
| 3449 | |
| 3450 | |
| 3451 | // The one member whose content this renderer does not build. `48a6e9e5`. |
| 3452 | // |
| 3453 | // The markup goes out as it arrived, which is the whole of what a canvas |
| 3454 | // is for and the whole of its risk: it is written by somebody who is not |
| 3455 | // the app, and the app is what sanitised it. Nothing here parses it, |
| 3456 | // because a second opinion formed from less context than the sanitiser |
| 3457 | // had is not a safety net -- it is a place for the two to disagree. |
| 3458 | // |
| 3459 | // The element carries the app's own class and id and none of this |
| 3460 | // renderer's. That is deliberate rather than an omission: the scope is a |
| 3461 | // contract between the app and the stylesheet it re-scoped, so a name |
| 3462 | // this crate prefixed or renamed would leave a sheet aimed at a selector |
| 3463 | // that matches nothing. They are escaped, being app strings in attribute |
| 3464 | // values, the same way `Document::body_class` is. |
| 3465 | Canvas => |
| 3466 | out.push_str; |
| 3467 | if let Some = &canvas.class |
| 3468 | out.push_str; |
| 3469 | escape_into; |
| 3470 | out.push; |
| 3471 | |
| 3472 | if let Some = &canvas.id |
| 3473 | out.push_str; |
| 3474 | escape_into; |
| 3475 | out.push; |
| 3476 | |
| 3477 | out.push; |
| 3478 | out.push_str; |
| 3479 | // After the markup and inside the scope, which is what lets a |
| 3480 | // creator's sheet reach the platform's own blocks. See |
| 3481 | // `Canvas::within`. |
| 3482 | for node in &canvas.within |
| 3483 | node_html; |
| 3484 | |
| 3485 | out.push_str; |
| 3486 | |
| 3487 | |
| 3488 | Region => slot_html, |
| 3489 | |
| 3490 | // A member added since this renderer last learned the vocabulary. |
| 3491 | // [`Node`] is `#[non_exhaustive]` so that landing one is not a lockstep |
| 3492 | // release across three renderers, and this arm is the price. |
| 3493 | // |
| 3494 | // It emits the same placeholder [`Node::StandIn`] does, because it is |
| 3495 | // the same situation said by the renderer instead of by the handler: |
| 3496 | // something is here and you are not seeing it. Emitting nothing would |
| 3497 | // leave a hole no reader could tell from a screen that never had the |
| 3498 | // part, and the markup and CSS for saying so already exist. |
| 3499 | // |
| 3500 | // `Readiness::Empty` and not `Failed`: nothing went wrong. The content |
| 3501 | // arrived intact and this renderer has not learned the word for it, so |
| 3502 | // a danger tone would report a fault the server did not have. |
| 3503 | _ => placeholder_html_into |
| 3504 | Empty, |
| 3505 | "Not shown here yet.", |
| 3506 | None, |
| 3507 | opts, |
| 3508 | out, |
| 3509 | , |
| 3510 | |
| 3511 | |
| 3512 | |
| 3513 | /// A readout the browser keeps writing. |
| 3514 | /// |
| 3515 | /// A `<span>` and not a `<time>`: a `<time>` element's text has to be a machine |
| 3516 | /// readable datetime when it carries no `datetime` attribute, and "3h ago" is |
| 3517 | /// not one. Carrying both would mean emitting the instant twice in two |
| 3518 | /// spellings for an element nothing in these apps styles differently. |
| 3519 | |
| 3520 | out.push_str; |
| 3521 | class_attr; |
| 3522 | let _ = write! |
| 3523 | out, |
| 3524 | " data-clock=\"{}\" data-at=\"{}\">" |
| 3525 | match clock |
| 3526 | Since => "since", |
| 3527 | Until => "until", |
| 3528 | Age => "age", |
| 3529 | , |
| 3530 | crateepoch_millis |
| 3531 | ); |
| 3532 | // Escaped like every other string this crate emits, even though the only |
| 3533 | // thing that can be here is digits and colons this file made. The rule is |
| 3534 | // that text goes through the escaper; an exception argued from what the |
| 3535 | // value happens to be today is how the next one is written by hand. |
| 3536 | escape_into |
| 3537 | &cratetext, |
| 3538 | out, |
| 3539 | ; |
| 3540 | out.push_str; |
| 3541 | |
| 3542 | |
| 3543 | /// A small labelled thing sitting inside something else. |
| 3544 | /// |
| 3545 | /// Took eight arguments, one per field of `Node::Token`, until the payload |
| 3546 | /// became [`Tag`] so a row could carry one. The `too_many_arguments` allow went |
| 3547 | /// with them. |
| 3548 | /// |
| 3549 | /// # What this renderer does with a hint |
| 3550 | /// |
| 3551 | /// Emits it as `title`. That is what the shipped goingson badges used and is |
| 3552 | /// the only standing-help affordance a browser gives a `span`, so a described |
| 3553 | /// badge draws what the hand-written one drew. It is hover-only and therefore |
| 3554 | /// unreachable by touch and unreliable to a screen reader, which is why |
| 3555 | /// [`Tag::hint`] says nothing may live only there. |
| 3556 | |
| 3557 | let Tag |
| 3558 | kind, |
| 3559 | label, |
| 3560 | tone, |
| 3561 | latched, |
| 3562 | action, |
| 3563 | hint, |
| 3564 | = tag; |
| 3565 | let = ; |
| 3566 | let action = action.as_ref; |
| 3567 | |
| 3568 | let mut classes = vec! |
| 3569 | Badge => "badge", |
| 3570 | Chip => "chip", |
| 3571 | ]; |
| 3572 | // `latched`, which is what makeover styles: `.chip.latched` is the pressed |
| 3573 | // depth a chip holds itself down with. This said `chip-latched`, a third |
| 3574 | // name for it, and a latched chip therefore looked exactly like an |
| 3575 | // unlatched one. |
| 3576 | if latched |
| 3577 | classes.push; |
| 3578 | |
| 3579 | |
| 3580 | // A badge answers no click, so it is not a button however it is styled. |
| 3581 | // The description says which through the kind, which is the whole reason |
| 3582 | // the two are separate members rather than one with a flag. |
| 3583 | let interactive = kind.interactive && action.is_some; |
| 3584 | let close = if interactive |
| 3585 | // An interactive tag whose destination leaves the app is an anchor, for |
| 3586 | // the reason `control_tag` gives. A tag that answers nothing stays a |
| 3587 | // span either way. |
| 3588 | let = action.map_or; |
| 3589 | out.push_str; |
| 3590 | close |
| 3591 | else |
| 3592 | out.push_str; |
| 3593 | "</span>" |
| 3594 | ; |
| 3595 | class_attr; |
| 3596 | tone_attr; |
| 3597 | |
| 3598 | if interactive |
| 3599 | // `data-act` for the same reason a button carries it: a chip that |
| 3600 | // answers a click is a control, and a table row filtering its own |
| 3601 | // trigger has to be able to tell one apart from its own text. A badge |
| 3602 | // never gets it, because a badge answers nothing. |
| 3603 | out.push_str; |
| 3604 | if latched |
| 3605 | // A chip standing for a filter is on or off, and its latched class |
| 3606 | // carries that fact visually through Depth::pressed either way. |
| 3607 | // Which word says it depends on what the chip turned out to be: |
| 3608 | // aria-pressed is a button's state and means nothing on an anchor, |
| 3609 | // and a link that is the view you are looking at is the one thing |
| 3610 | // aria-current exists to say. |
| 3611 | if action.is_some_and |
| 3612 | out.push_str; |
| 3613 | else |
| 3614 | out.push_str; |
| 3615 | |
| 3616 | |
| 3617 | if let Some = action |
| 3618 | action_attrs; |
| 3619 | |
| 3620 | |
| 3621 | |
| 3622 | // `436bc223`: the detail behind the label, as `title`, which is what the |
| 3623 | // shipped goingson badges use and is the only standing-help affordance a |
| 3624 | // browser gives a span. Hover-only, so it is genuinely a hint and the |
| 3625 | // vocabulary says so -- nothing may live only here. |
| 3626 | if let Some = hint |
| 3627 | out.push_str; |
| 3628 | escape_into; |
| 3629 | out.push; |
| 3630 | |
| 3631 | |
| 3632 | out.push; |
| 3633 | escape_into; |
| 3634 | if matches! |
| 3635 | out.push_str; |
| 3636 | class_attr; |
| 3637 | out.push_str; |
| 3638 | |
| 3639 | out.push_str; |
| 3640 | |
| 3641 | |
| 3642 | /// What tells the swap to leave this element alone. |
| 3643 | /// |
| 3644 | /// The third function `hx-` is allowed to appear in, and it is here rather than |
| 3645 | /// inlined for the reason the other two are named: decision 13 says the |
| 3646 | /// transport is replaceable, and a claim like that is only worth anything if |
| 3647 | /// something checks it. `htmx_enters_in_exactly_two_functions` is that check |
| 3648 | /// and it counts three now. |
| 3649 | /// |
| 3650 | /// A third and not a fourth reading of one fact. [`action_attrs`] says where a |
| 3651 | /// control sends and where its answer lands, [`oob_html`] says where a piece of |
| 3652 | /// the answer lands that no control asked for, and this says what the answer |
| 3653 | /// may not touch on the way in. Swapping htmx for something else rewrites the |
| 3654 | /// three of them together. |
| 3655 | /// |
| 3656 | /// Written without a leading space, which is [`Filling::control_attrs`]'s |
| 3657 | /// contract: the caller separates them. |
| 3658 | /// |
| 3659 | /// [`Filling::control_attrs`]: makeover_webview::form::Filling::control_attrs |
| 3660 | |
| 3661 | out.push_str; |
| 3662 | |
| 3663 | |
| 3664 | /// A region the answer changed without being aimed at it. |
| 3665 | /// |
| 3666 | /// The second half of the transport, and the reason this file's htmx test |
| 3667 | /// names two functions rather than one. [`action_attrs`] says where a control |
| 3668 | /// sends and where the answer lands; this says where a piece of the answer |
| 3669 | /// lands that no control asked for. Both are the same fact — how htmx is told |
| 3670 | /// to put markup somewhere — and a transport swapped for fixi moves both, which |
| 3671 | /// is what decision 13's claim needs. |
| 3672 | /// |
| 3673 | /// # Why `innerHTML:` and not a bare `true` |
| 3674 | /// |
| 3675 | /// A bare `hx-swap-oob="true"` replaces the element carrying the matching id |
| 3676 | /// outright, and the element in the document is [`slot_html`]'s `<div>` with |
| 3677 | /// the region's classes on it. Replacing it with what is emitted here would |
| 3678 | /// strip them, so the region would keep its contents and lose its layout. |
| 3679 | /// Addressing the swap by selector instead puts the markup *inside* the slot |
| 3680 | /// and leaves the wrapper alone, which is what [`Serves::fragment`] already |
| 3681 | /// means for the targeted region. The wrapper emitted here is htmx's envelope |
| 3682 | /// and never reaches the document. |
| 3683 | /// |
| 3684 | /// [`Serves::fragment`]: quasi_http::Serves::fragment |
| 3685 | pub |
| 3686 | out.push_str; |
| 3687 | // Escaped as an attribute, and otherwise untouched for `slot_html`'s |
| 3688 | // reason: this has to match the id that function emitted, and a slot id is |
| 3689 | // the address the description chose. |
| 3690 | escape_into; |
| 3691 | out.push_str; |
| 3692 | node_html; |
| 3693 | out.push_str; |
| 3694 | |
| 3695 | |
| 3696 | /// Write a frame's `class="..."`, prefixed, plus `current` if it is the one, |
| 3697 | /// plus what addresses it. |
| 3698 | /// |
| 3699 | /// `current` is written bare rather than through [`class_attr`], because |
| 3700 | /// `showing_rules` writes `.showing-frame:not(.current)` and the state half of |
| 3701 | /// that pair does not move under a prefix. Same shape as makeover's `chosen` |
| 3702 | /// and `latched`: the thing is prefixed, the state qualifying it is not. |
| 3703 | /// |
| 3704 | /// `data-frame` and `data-shown` are what the emitted chrome selects on: |
| 3705 | /// which region the frame belongs to, and which of that region's children it |
| 3706 | /// is. The region is named on every frame rather than the frames being found by |
| 3707 | /// descent, because a region showing one child at a time can contain another |
| 3708 | /// one -- a tab group over a gallery -- and a descendant query would step the |
| 3709 | /// outer region through the inner one's frames. |
| 3710 | /// |
| 3711 | /// Attributes and not classes for `data-shows`' reason: these are addresses |
| 3712 | /// this renderer emits for its own chrome, and a class is the app's styling |
| 3713 | /// surface. |
| 3714 | |
| 3715 | out.push_str; |
| 3716 | class_into; |
| 3717 | if current |
| 3718 | out.push_str; |
| 3719 | |
| 3720 | out.push_str; |
| 3721 | escape_into; |
| 3722 | let _ = write!; |
| 3723 | |
| 3724 | |
| 3725 | /// Who asks for a region's content, when the region says where it comes from. |
| 3726 | /// |
| 3727 | /// A labelled tab group is the second: the strip button carries the panel's |
| 3728 | /// address, so the panel must not carry it as well or pressing a tab would |
| 3729 | /// fetch it twice and the four unpressed ones would fetch on load. |
| 3730 | /// |
| 3731 | /// A two-member enum rather than a `bool`, because the call sites that pass it |
| 3732 | /// are twenty lines apart and `slot_html(child, false, ..)` says nothing about |
| 3733 | /// which false. |
| 3734 | |
| 3735 | pub |
| 3736 | /// The region asks for its own content, which is every region but one. |
| 3737 | ForItself, |
| 3738 | /// Something above it asks, so it stays silent and waits to be given. |
| 3739 | AndIsAskedFor, |
| 3740 | |
| 3741 | |
| 3742 | /// The address of one child of a region that shows them one at a time. |
| 3743 | /// |
| 3744 | /// `Slot::fed_by` on such a child means the panel is a route, and the strip is |
| 3745 | /// what calls it, so this is read from the parent rather than acted on by the |
| 3746 | /// child. Every other region reads `fed_by` the ordinary way: it is the parent |
| 3747 | /// showing one child at a time that changes who asks. |
| 3748 | |
| 3749 | match &slot.body.get?.node |
| 3750 | Region => child.fed_by.as_deref, |
| 3751 | _ => None, |
| 3752 | |
| 3753 | |
| 3754 | |
| 3755 | /// A region whose children are answers to one question, and the chrome for it. |
| 3756 | /// |
| 3757 | /// [`Slot::repeating`] says the children are slots; this draws each under its |
| 3758 | /// number, with the control that takes it away, and the control that adds one |
| 3759 | /// under the lot. |
| 3760 | /// |
| 3761 | /// # Everything here is derived as nodes and rendered through the ordinary walk |
| 3762 | /// |
| 3763 | /// The caption is a [`Node::Heading`] and both controls are [`Node::Act`], so |
| 3764 | /// this function invents no markup and no class: a slot's number comes out as |
| 3765 | /// the same heading a described one does, and the two controls take everything |
| 3766 | /// [`act_html`] already knows about tone, confirmation and waiting. The |
| 3767 | /// alternative was a `slot-caption` class this crate defined and makeover did |
| 3768 | /// not, which is the SSH-keys bug the class-coverage test exists to catch. |
| 3769 | /// |
| 3770 | /// # The floor and the ceiling are drawn, not hidden |
| 3771 | /// |
| 3772 | /// A remove past the floor and an add past the ceiling are emitted disabled |
| 3773 | /// rather than left out. audiofiles' rule editor already made that call for its |
| 3774 | /// last condition -- "offered dead rather than hidden", following its own |
| 3775 | /// last-vault Delete -- and a control that vanishes at a boundary is a control |
| 3776 | /// the reader has to discover twice. |
| 3777 | /// |
| 3778 | /// What changed is who enforces it. The app disabled its own last Remove and |
| 3779 | /// every renderer would have had to be told separately; now |
| 3780 | /// [`Repeating::least`] says it once and [`Repeating::may_remove`] is what all |
| 3781 | /// three read. |
| 3782 | |
| 3783 | slot: &Slot, |
| 3784 | repeating: &Repeating, |
| 3785 | opts: &Emit, |
| 3786 | doc: &, |
| 3787 | out: &mut String, |
| 3788 | |
| 3789 | let standing = slot.body.len; |
| 3790 | for in slot.body.iter.enumerate |
| 3791 | // One-based, because it is read by a person: "Condition 1" and not |
| 3792 | // "Condition 0". The number is the renderer's for the reason |
| 3793 | // `Repeating::one` is a singular noun -- a description that wrote the |
| 3794 | // numbers would go stale the moment a slot left the middle. |
| 3795 | node_html |
| 3796 | §ion, |
| 3797 | opts, |
| 3798 | doc, |
| 3799 | out, |
| 3800 | ; |
| 3801 | ranked_html; |
| 3802 | |
| 3803 | // The child's own, because only the child knows which slot it is. See |
| 3804 | // `Slot::removes` for why it is not one action on the parent. |
| 3805 | if let Region = &placed.node |
| 3806 | && let Some = &child.removes |
| 3807 | |
| 3808 | let mut act = .clone; |
| 3809 | if !repeating.may_remove |
| 3810 | act = act.disabled; |
| 3811 | |
| 3812 | act_html; |
| 3813 | |
| 3814 | |
| 3815 | |
| 3816 | let mut add = repeating.add.clone; |
| 3817 | if !repeating.may_add |
| 3818 | add = add.disabled; |
| 3819 | |
| 3820 | act_html; |
| 3821 | |
| 3822 | |
| 3823 | /// The chrome for a region showing one child at a time. |
| 3824 | /// |
| 3825 | /// Derived, once, for every widget there will ever be. Nothing here reads |
| 3826 | /// [`quasi_router::RegionKind::Widget`]'s name, and that is the point of the |
| 3827 | /// whole design: a carousel, a tab group and a disclosure are one region that |
| 3828 | /// shows some of its children, and which idiom comes out falls out of what the |
| 3829 | /// children carry rather than out of what the assembly is called. |
| 3830 | /// |
| 3831 | /// # The three shapes, and what picks between them |
| 3832 | /// |
| 3833 | /// - Children carrying labels get a strip of them, which is |
| 3834 | /// [`layout::Selector::Tabs`] markup verbatim. A tab strip is already a |
| 3835 | /// described thing here; deriving a second spelling of one would be this |
| 3836 | /// renderer inventing a name makeover would then not style, which is the |
| 3837 | /// SSH-keys bug with extra steps. |
| 3838 | /// - One dismissible child with a name gets that name as a control, which is a |
| 3839 | /// summary line that opens. |
| 3840 | /// - Anything else gets previous, position, next. |
| 3841 | /// |
| 3842 | /// # Where the bytes come from, and what picks between the two |
| 3843 | /// |
| 3844 | /// A carousel's frames are in the document already, so its controls call no |
| 3845 | /// route: what changes is which of several children the reader has downloaded |
| 3846 | /// is showing, and a round trip to reveal those bytes is worse on every one of |
| 3847 | /// the three MNW galleries this was measured against. Those controls carry |
| 3848 | /// `data-shows` and whatever binds the region binds them, the same relationship |
| 3849 | /// `data-act` and `data-bespoke` already have with their host. |
| 3850 | /// |
| 3851 | /// A tab group over routed panels is the other case, `dfbc88ce`. When a child |
| 3852 | /// carries [`Slot::fed_by`], that action is the tab's address and the strip |
| 3853 | /// button calls it, so the reader downloads the panel they asked for and not |
| 3854 | /// the four they did not. Both marks go on the button: htmx makes the request |
| 3855 | /// and the binder moves the frame, which is the same division of labour as a |
| 3856 | /// control that acts and navigates. |
| 3857 | /// |
| 3858 | /// The child does not also fetch itself, which is the half of this that is not |
| 3859 | /// visible from here -- see [`slot_html`]. It is uniform rather than sparing |
| 3860 | /// the shown child because a screen renders once at final geometry, with no |
| 3861 | /// placeholder before first content, and the shown panel arriving on `load` is |
| 3862 | /// a placeholder on the one panel being looked at. |
| 3863 | /// |
| 3864 | /// Presence is the only thing that picks between the two, so nothing here reads |
| 3865 | /// the region's kind and a description that mixes them is describing a carousel |
| 3866 | /// whose frames are routes, which is a coherent thing to want. |
| 3867 | |
| 3868 | let labels = slot.labels; |
| 3869 | let current = slot.current; |
| 3870 | let total = slot.body.len; |
| 3871 | // What performs the move, when there is something that can address the |
| 3872 | // region. `0081563e`: the frames are in the document, so moving between |
| 3873 | // them is local, and this is the renderer emitting the behaviour instead of |
| 3874 | // waiting for a host to bind one. See `crate::hyperscript` for the whole of |
| 3875 | // why a program may be written into an attribute and why an id that is not |
| 3876 | // a handle gets the mark and nothing else. |
| 3877 | let handle = cratehandle; |
| 3878 | |
| 3879 | // A named single child that can close is a disclosure, and the check comes |
| 3880 | // first because such a child is also a labelled one: a strip of one tab is |
| 3881 | // not what a summary line is. |
| 3882 | if slot.showing.dismissible |
| 3883 | && total == 1 |
| 3884 | && let = labels.as_slice |
| 3885 | |
| 3886 | out.push_str; |
| 3887 | // `button`, which is makeover's name for a control and carries its whole |
| 3888 | // interactive set. `act_html` learned this the expensive way. |
| 3889 | class_attr; |
| 3890 | out.push_str; |
| 3891 | out.push_str |
| 3892 | "true\"" |
| 3893 | else |
| 3894 | "false\"" |
| 3895 | ; |
| 3896 | if let Some = handle |
| 3897 | cratedisclosure; |
| 3898 | |
| 3899 | out.push; |
| 3900 | escape_into; |
| 3901 | out.push_str; |
| 3902 | return; |
| 3903 | |
| 3904 | |
| 3905 | if !labels.is_empty |
| 3906 | let tab = option_class; |
| 3907 | out.push_str; |
| 3908 | class_attr; |
| 3909 | out.push_str; |
| 3910 | out.push_str; |
| 3911 | out.push_str; |
| 3912 | // `aad33ecc`. The strip is the element carrying `role="tablist"`, so it |
| 3913 | // is the element a screen reader announces and the one the region's own |
| 3914 | // name belongs on. `slot_html` skips the name when it emits a strip, so |
| 3915 | // the two never both write it. |
| 3916 | name_attr; |
| 3917 | out.push; |
| 3918 | for in labels.iter.enumerate |
| 3919 | let picked = current == Some; |
| 3920 | out.push_str; |
| 3921 | class_into; |
| 3922 | if picked |
| 3923 | out.push_str; |
| 3924 | |
| 3925 | let _ = write!; |
| 3926 | out.push_str; |
| 3927 | // The panel's address, when the panel is a route rather than bytes |
| 3928 | // already here. No target is written, for the reason `slot_html` |
| 3929 | // does not write one either: the router answers with a fragment |
| 3930 | // naming the slot it changed and aims it with `HX-Retarget`, so the |
| 3931 | // party that knows stays the party that says. |
| 3932 | if let Some = fed_child |
| 3933 | action_attrs; |
| 3934 | |
| 3935 | // Last, after whatever transport this tab carries, because the two |
| 3936 | // are separate halves of one press and reading them apart is how |
| 3937 | // the markup stays readable: htmx fetches the panel if there is one |
| 3938 | // to fetch, and this moves the frame either way. |
| 3939 | if let Some = handle |
| 3940 | cratetab; |
| 3941 | |
| 3942 | out.push; |
| 3943 | escape_into; |
| 3944 | out.push_str; |
| 3945 | |
| 3946 | out.push_str; |
| 3947 | return; |
| 3948 | |
| 3949 | |
| 3950 | // Previous, position, next. In flow, under the content, nothing overlaid: |
| 3951 | // a terminal cannot honestly overlay anything, and the dot strip this |
| 3952 | // replaces had no form at all past a handful of children -- two of the |
| 3953 | // three galleries it shipped on are creator uploads of arbitrary length. |
| 3954 | out.push_str; |
| 3955 | class_attr; |
| 3956 | out.push; |
| 3957 | |
| 3958 | out.push_str; |
| 3959 | class_attr; |
| 3960 | out.push_str; |
| 3961 | if let Some = handle |
| 3962 | cratestep; |
| 3963 | |
| 3964 | out.push_str; |
| 3965 | |
| 3966 | out.push; |
| 3967 | out.push_str; |
| 3968 | class_attr; |
| 3969 | // Named as well as classed, because the step controls write into it: a |
| 3970 | // class is what a stylesheet is entitled to rename, and this is an address. |
| 3971 | if let Some = handle |
| 3972 | out.push_str; |
| 3973 | escape_into; |
| 3974 | out.push; |
| 3975 | |
| 3976 | // Zero when a dismissible region is closed, which is a true statement about |
| 3977 | // how many of its children are showing and needs no glyph of its own. |
| 3978 | let _ = write!; |
| 3979 | |
| 3980 | out.push_str; |
| 3981 | class_attr; |
| 3982 | out.push_str; |
| 3983 | if let Some = handle |
| 3984 | cratestep; |
| 3985 | |
| 3986 | out.push_str; |
| 3987 | |
| 3988 | out.push_str; |
| 3989 | |
| 3990 | |
| 3991 | /// The chrome for a set that arrived in parts. |
| 3992 | /// |
| 3993 | /// The same control [`showing_html`] draws, bound the other way. A carousel's |
| 3994 | /// frames are in the document already, so its buttons carry `data-shows` and the |
| 3995 | /// move is the host's; these rows were never fetched, so the buttons carry |
| 3996 | /// addresses and the move is a request. Presence is the only difference, which |
| 3997 | /// is why `quasi_router::Rest` is a `layout::Paging` plus addresses rather than |
| 3998 | /// a second kind of position. |
| 3999 | /// |
| 4000 | /// # The two labels |
| 4001 | /// |
| 4002 | /// A set with a page size prints "3 / 8", which is `showing_html`'s spelling of |
| 4003 | /// position and deliberately the same one. A set without prints what it has: |
| 4004 | /// "150 of 400", or bare "Show more" when nothing counted it. The description |
| 4005 | /// decides which by whether it is paged, and no renderer infers it from the |
| 4006 | /// numbers. |
| 4007 | /// |
| 4008 | /// # A direction with no address is left out, not drawn disabled |
| 4009 | /// |
| 4010 | /// Ruled by Max 2026-09-08, reversing what stood here. The argument for the |
| 4011 | /// disabled control was "first paint is final paint": a pager that gains a |
| 4012 | /// button on page two moves everything beside it. The argument against is this |
| 4013 | /// renderer's own, written under [`rest_strip_html`] about the page a reader is |
| 4014 | /// already on -- a control that answers nothing is the lying control |
| 4015 | /// [`control_tag`] exists to prevent -- and a Prev that cannot go back is that |
| 4016 | /// control. The layout shift is the price, and `.rest` is the place to pay it |
| 4017 | /// if it turns out to matter. |
| 4018 | /// |
| 4019 | /// It is also what makes a paged screen compilable, which is not why it was |
| 4020 | /// decided but is why it was asked. A guard derives as a branch only when |
| 4021 | /// turning it off deletes bytes; a disabled control substitutes for an enabled |
| 4022 | /// one, and the derivation silently baked whichever it happened to render. See |
| 4023 | /// wiki `quasi-declare-form` section 29. |
| 4024 | |
| 4025 | let paging = rest.as_layout; |
| 4026 | |
| 4027 | out.push_str; |
| 4028 | class_attr; |
| 4029 | out.push; |
| 4030 | |
| 4031 | // Back, then the jumps, then forward, matching `Rest::placed`. A pager is |
| 4032 | // the container whose declaration order and render order genuinely differ: |
| 4033 | // the renderer puts Prev first whatever order the declaration said its |
| 4034 | // directions in, which is what `emit::pager_order` makes true rather than |
| 4035 | // hoped for. |
| 4036 | let mut marked = crateopen; |
| 4037 | |
| 4038 | if let Some = &rest.back |
| 4039 | marked.starts; |
| 4040 | let = control_tag; |
| 4041 | out.push_str; |
| 4042 | class_attr; |
| 4043 | action_attrs; |
| 4044 | out.push_str; |
| 4045 | out.push_str; |
| 4046 | marked.ends; |
| 4047 | |
| 4048 | |
| 4049 | if rest.jumps.is_empty |
| 4050 | out.push; |
| 4051 | out.push_str; |
| 4052 | class_attr; |
| 4053 | out.push; |
| 4054 | match |
| 4055 | => |
| 4056 | let _ = write!; |
| 4057 | |
| 4058 | _ => match paging.total |
| 4059 | Some => |
| 4060 | let _ = write!; |
| 4061 | |
| 4062 | None => out.push_str, |
| 4063 | , |
| 4064 | |
| 4065 | out.push_str; |
| 4066 | else |
| 4067 | rest_strip_html; |
| 4068 | |
| 4069 | |
| 4070 | // Written out rather than shared with the Prev branch above. The class name |
| 4071 | // has to sit in the `class_attr` call as a literal or `tests/vocabulary.rs` |
| 4072 | // cannot see it, and a renderer whose emitted names are invisible to that |
| 4073 | // scan is how the SSH-keys drift came back. The duplication is the price of |
| 4074 | // the check working. |
| 4075 | if let Some = &rest.forward |
| 4076 | marked.starts; |
| 4077 | let = control_tag; |
| 4078 | out.push_str; |
| 4079 | class_attr; |
| 4080 | action_attrs; |
| 4081 | out.push_str; |
| 4082 | out.push_str; |
| 4083 | marked.ends; |
| 4084 | |
| 4085 | |
| 4086 | marked.close; |
| 4087 | out.push_str; |
| 4088 | |
| 4089 | |
| 4090 | /// The numbered pages, where the description offered any. |
| 4091 | /// |
| 4092 | /// In place of the "3 / 8" readout rather than beside it: the strip says both |
| 4093 | /// numbers already -- the marked one is where the reader is and the last is |
| 4094 | /// how many there are -- and printing the position twice is a control arguing |
| 4095 | /// with itself. |
| 4096 | /// |
| 4097 | /// The page the reader is on is drawn as text and not as a control, which is |
| 4098 | /// what `templates/pages/feed.html` already did with a `<span class="current">`. |
| 4099 | /// A button that reloads the page it is on is an affordance that does nothing, |
| 4100 | /// and a control that answers nothing is the lying control `control_tag` exists |
| 4101 | /// to prevent. |
| 4102 | /// |
| 4103 | /// A description that offered a strip the reader is not in draws every page as |
| 4104 | /// a control and marks none, which is a host that windowed its pages wrong. It |
| 4105 | /// is worth being able to see, and it is not worth refusing to draw a list |
| 4106 | /// over. |
| 4107 | |
| 4108 | out.push_str; |
| 4109 | class_attr; |
| 4110 | out.push; |
| 4111 | for jump in &rest.jumps |
| 4112 | marked.starts; |
| 4113 | if jump.here |
| 4114 | out.push_str; |
| 4115 | class_attr; |
| 4116 | out.push_str; |
| 4117 | let _ = write!; |
| 4118 | out.push_str; |
| 4119 | marked.ends; |
| 4120 | continue; |
| 4121 | |
| 4122 | let = control_tag; |
| 4123 | out.push_str; |
| 4124 | class_attr; |
| 4125 | action_attrs; |
| 4126 | out.push; |
| 4127 | let _ = write!; |
| 4128 | out.push_str; |
| 4129 | marked.ends; |
| 4130 | |
| 4131 | out.push_str; |
| 4132 | |
| 4133 | |
| 4134 | /// A region, and everything under it. |
| 4135 | /// The region's leading row: whatever the region puts there itself, then the |
| 4136 | /// members the description said share it. |
| 4137 | /// |
| 4138 | /// Ruling: wiki `layout-room-and-fallback`. What this replaces is goingson |
| 4139 | /// pinning a toolbar over a tab strip in a stylesheet, so the property worth |
| 4140 | /// stating is the one the wrapper buys: both are inside one flex row with a |
| 4141 | /// min-content floor under each, which is `makeover-webview`'s `.run`. Nothing |
| 4142 | /// here positions anything, and nothing here names a size. |
| 4143 | /// |
| 4144 | /// `strip` is the caller's, not recomputed here: a tab group's strip and a |
| 4145 | /// carousel's counter row are both `showing_html`, they sit on opposite sides |
| 4146 | /// of the frames, and only the caller knows which side it is on. A region with |
| 4147 | /// no run and no strip writes nothing at all, which is what every region |
| 4148 | /// written before either existed did. |
| 4149 | |
| 4150 | let Some = slot.run.as_ref else |
| 4151 | if strip |
| 4152 | showing_html; |
| 4153 | |
| 4154 | return; |
| 4155 | ; |
| 4156 | |
| 4157 | // The wrapper appears only when there is a run, so the strip a tab group |
| 4158 | // has always emitted is unwrapped until the description says something |
| 4159 | // shares its row. That keeps the change additive in the markup as well as |
| 4160 | // in the type. |
| 4161 | out.push_str; |
| 4162 | out.push_str; |
| 4163 | out.push; |
| 4164 | out.push_str; |
| 4165 | out.push_str; |
| 4166 | if strip |
| 4167 | showing_html; |
| 4168 | |
| 4169 | for placed in &run.members |
| 4170 | ranked_html; |
| 4171 | |
| 4172 | out.push_str; |
| 4173 | |
| 4174 | |
| 4175 | /// A region's own accessible name, when it has one. |
| 4176 | /// |
| 4177 | /// `aria-label` rather than a visible caption: the description says the region |
| 4178 | /// is called this, and a region that also wants it on the screen carries a |
| 4179 | /// [`Heading`](quasi_router::layout::Heading) in its body. Writing both from one |
| 4180 | /// member would be this renderer deciding a screen's copy. |
| 4181 | |
| 4182 | if let Some = name |
| 4183 | out.push_str; |
| 4184 | escape_into; |
| 4185 | out.push; |
| 4186 | |
| 4187 | |
| 4188 | |
| 4189 | pub |
| 4190 | // A region's own questions, each on a wrapper outside the region rather |
| 4191 | // than on the region itself. Two reasons, and the first is the one that |
| 4192 | // makes it mandatory: htmx takes one verb and one address per element, and |
| 4193 | // a region that also names a `fed_by` already spent its. The second is |
| 4194 | // `Field::consults`' -- one wrapper each, so a panel recomputing from one |
| 4195 | // set of dials at two rates is two questions and not one that overwrote the |
| 4196 | // other. |
| 4197 | // |
| 4198 | // Outside rather than inside, so the gathered set is exactly the region's |
| 4199 | // contents: the `find input, find select, find textarea` this trigger |
| 4200 | // carries reaches the whole region from here, and from inside it would |
| 4201 | // reach whatever the wrapper happened to enclose. |
| 4202 | for consult in &slot.consults |
| 4203 | out.push_str; |
| 4204 | class_attr; |
| 4205 | // What rides along from outside the region, named by field name. The |
| 4206 | // dials inside need no naming: they are what the trigger's own include |
| 4207 | // gathers, which is the whole of what moving the consult up to a region |
| 4208 | // bought. |
| 4209 | let sends = sends_selector; |
| 4210 | action_attrs |
| 4211 | &consult.action, |
| 4212 | Working |
| 4213 | after: consult.after, |
| 4214 | at_least: consult.at_least, |
| 4215 | , |
| 4216 | None, |
| 4217 | sends.as_deref, |
| 4218 | false, |
| 4219 | out, |
| 4220 | ; |
| 4221 | out.push; |
| 4222 | |
| 4223 | |
| 4224 | let kind = match &slot.kind |
| 4225 | Band => "band", |
| 4226 | Sidebar => "sidebar", |
| 4227 | Pane => "pane", |
| 4228 | Group => "group", |
| 4229 | TabGroup => "tabgroup", |
| 4230 | Modal => "modal", |
| 4231 | Handover | Ceded => |
| 4232 | "bespoke" |
| 4233 | |
| 4234 | Widget => "widget", |
| 4235 | ; |
| 4236 | |
| 4237 | // Every region is a `div` except the one whose whole meaning is "these |
| 4238 | // things belong together", which is what `section` means in HTML. This is |
| 4239 | // the renderer picking the expression its host has for an intent the |
| 4240 | // description states, the same way a terminal picks a rule and egui picks a |
| 4241 | // frame; nothing in the description asked for an element name. |
| 4242 | // |
| 4243 | // Only `Group`, deliberately. A `section` per pane or per band would be the |
| 4244 | // usual mistake of reaching for the semantic element because it sounds |
| 4245 | // better, and a document of nested sections says less than one that names |
| 4246 | // the one thing it means. |
| 4247 | let tag = match &slot.kind |
| 4248 | Group => "section", |
| 4249 | _ => "div", |
| 4250 | ; |
| 4251 | |
| 4252 | let _ = write!; |
| 4253 | // The id is the fragment's address. It is escaped and otherwise untouched: |
| 4254 | // rewriting it would break the HX-Retarget the router just sent, which |
| 4255 | // names the slot's own id. |
| 4256 | escape_into; |
| 4257 | out.push; |
| 4258 | class_attr; |
| 4259 | |
| 4260 | if let Handover | Ceded = |
| 4261 | &slot.kind |
| 4262 | |
| 4263 | // Never interpreted, per decision 4. Handed to the app under a name it |
| 4264 | // chose, which is the entire contract for a bespoke region. |
| 4265 | out.push_str; |
| 4266 | escape_into; |
| 4267 | out.push; |
| 4268 | |
| 4269 | |
| 4270 | if let Widget = &slot.kind |
| 4271 | // Also never interpreted here, and for a different purpose: this is the |
| 4272 | // hook a stylesheet or a script attaches to in order to draw the |
| 4273 | // assembly the way a browser does it. It is an attribute rather than a |
| 4274 | // class because a widget name is app vocabulary and classes here are |
| 4275 | // this renderer's. |
| 4276 | // |
| 4277 | // What makes it safe to emit and ignore is that the body under it is |
| 4278 | // already the whole assembly in primitives. Nothing recognises |
| 4279 | // `data-widget` and the region still renders -- a carousel degrades to |
| 4280 | // its frames, which is the JS-off fallback the shipped partial has and |
| 4281 | // the reason the tier does not need three renderers to release in step. |
| 4282 | out.push_str; |
| 4283 | escape_into; |
| 4284 | out.push; |
| 4285 | |
| 4286 | |
| 4287 | // What a binder looks for, and the one attribute that says this region has |
| 4288 | // chrome to bind. It is not the widget's name: a script that moved between |
| 4289 | // a carousel's frames by matching `data-widget="carousel"` would have to be |
| 4290 | // written again for the next assembly, which is the per-widget code the |
| 4291 | // whole derivation exists to stop. |
| 4292 | // |
| 4293 | // Nothing is emitted for `Showing::All`, so the hook is present exactly |
| 4294 | // when there is something to bind. |
| 4295 | match slot.showing |
| 4296 | One => out.push_str, |
| 4297 | AtMostOne => out.push_str, |
| 4298 | _ => |
| 4299 | |
| 4300 | |
| 4301 | // What brings the region out, when it is not simply out. `079a011e`: the |
| 4302 | // condition is the region's own, so it is emitted here rather than on |
| 4303 | // whatever control the reveal script will read. |
| 4304 | // |
| 4305 | // Nothing decides the initial state on this side. The emitter sees one node |
| 4306 | // at a time and the control may be anywhere on the screen or, for a |
| 4307 | // fragment, not in this response at all; the script settles every |
| 4308 | // conditional region on parse and after every swap, which is one answer for |
| 4309 | // both. The cost is that a document served without the script shows them |
| 4310 | // all, which is stated where the script is. |
| 4311 | if let Some = &slot.revealed_by |
| 4312 | reveal_attrs; |
| 4313 | |
| 4314 | |
| 4315 | if matches! && matches! |
| 4316 | out.push_str; |
| 4317 | |
| 4318 | |
| 4319 | // The region says where its content is coming from, so it asks for it |
| 4320 | // itself as soon as it exists. This is what a hand-split route was doing |
| 4321 | // before there was a word for it: MNW's payout summary is its own tab |
| 4322 | // because it calls a payment provider while the rest of the screen reads the |
| 4323 | // database, and under this it is a region of the screen that arrives late. |
| 4324 | // |
| 4325 | // Every `hx-` attribute comes out of `action_attrs`, including these. The |
| 4326 | // answer is aimed by `HX-Retarget` naming this slot's id, which is why |
| 4327 | // nothing here writes a target: the router is the party that knows what it |
| 4328 | // changed, and that is unchanged by the request having been started here. |
| 4329 | // |
| 4330 | // Unless something above it is asking on its behalf, which is |
| 4331 | // [`Asks::AndIsAskedFor`] and is one case: a panel of a labelled tab group, |
| 4332 | // whose strip button carries this same address. Both halves are suppressed |
| 4333 | // together, `aria-busy` above included, because a panel nobody has asked |
| 4334 | // for is not waiting on anything. |
| 4335 | if matches! |
| 4336 | && let Some = &slot.fed_by |
| 4337 | |
| 4338 | // A live region asks on a cadence instead of once. The description says |
| 4339 | // only that the contents move; the interval is `crate::CADENCE`, picked |
| 4340 | // for every screen this crate draws rather than per template, which is |
| 4341 | // what MNW's two hand-written `every 10s` triggers were doing without |
| 4342 | // anything making them comparable. |
| 4343 | let fires = if slot.live else ; |
| 4344 | action_attrs; |
| 4345 | |
| 4346 | |
| 4347 | if matches! |
| 4348 | // A modal takes input until dismissed, which is what modal means, and |
| 4349 | // saying so is the renderer's job rather than the app's. |
| 4350 | out.push_str; |
| 4351 | |
| 4352 | |
| 4353 | // `aad33ecc`. The region's own name, on the region itself -- unless a tab |
| 4354 | // strip is about to be emitted, in which case the strip is the element with |
| 4355 | // the role and `showing_html` writes it there instead. A named `div` with no |
| 4356 | // role is announced by nothing, so putting it in both places would be one |
| 4357 | // announcement and one wasted attribute rather than two announcements. |
| 4358 | if !slot.showing.selective || slot.labels.is_empty |
| 4359 | name_attr; |
| 4360 | |
| 4361 | |
| 4362 | out.push; |
| 4363 | if slot.showing.selective |
| 4364 | let current = slot.current; |
| 4365 | let labelled = !slot.labels.is_empty; |
| 4366 | |
| 4367 | // A strip sits above the panes it opens and a counter row sits under |
| 4368 | // the content it counts. That is the only placement decision here, and |
| 4369 | // it is the folder semantic rather than a preference: a tab that came |
| 4370 | // after its pane would not read as the tab of it. |
| 4371 | if labelled |
| 4372 | run_html; |
| 4373 | |
| 4374 | |
| 4375 | // Each child is wrapped, because the rule that collapses the stack has |
| 4376 | // to have something to select and a described child emits whatever |
| 4377 | // element it is. The wrapper appears only here, so a region that shows |
| 4378 | // everything -- which is every region written before `Showing` existed |
| 4379 | // -- emits exactly the markup it always did. |
| 4380 | let mut marked = crateopen; |
| 4381 | for in slot.body.iter.enumerate |
| 4382 | marked.starts; |
| 4383 | out.push_str; |
| 4384 | frame_attrs; |
| 4385 | out.push; |
| 4386 | // A labelled strip has just emitted every panel's address on its |
| 4387 | // own buttons, so the panels are asked for rather than asking. The |
| 4388 | // drop-class wrapper is rebuilt here rather than reached through |
| 4389 | // `ranked_html`, which has no reason to learn about this and no |
| 4390 | // other caller that would pass it. |
| 4391 | match |
| 4392 | => |
| 4393 | let drops = drop_class; |
| 4394 | if let Some = drops |
| 4395 | out.push_str; |
| 4396 | class_attr; |
| 4397 | out.push; |
| 4398 | |
| 4399 | slot_html; |
| 4400 | if drops.is_some |
| 4401 | out.push_str; |
| 4402 | |
| 4403 | |
| 4404 | _ => ranked_html, |
| 4405 | |
| 4406 | out.push_str; |
| 4407 | marked.ends; |
| 4408 | |
| 4409 | marked.close; |
| 4410 | |
| 4411 | if !labelled |
| 4412 | run_html; |
| 4413 | |
| 4414 | else if let Some = &slot.repeating |
| 4415 | run_html; |
| 4416 | repeating_html; |
| 4417 | else |
| 4418 | run_html; |
| 4419 | let mut marked = crateopen; |
| 4420 | for placed in slot.body.iter |
| 4421 | marked.starts; |
| 4422 | ranked_html; |
| 4423 | marked.ends; |
| 4424 | |
| 4425 | marked.close; |
| 4426 | |
| 4427 | |
| 4428 | // The host's markup for this region, after whatever the description put |
| 4429 | // here, and only for a bespoke one: a fill named against a pane is a host |
| 4430 | // reaching into a region the description already owns. |
| 4431 | // |
| 4432 | // Verbatim. See `Webview::fills` for why that is not the hole it looks |
| 4433 | // like: the string is host code's, never a description's, so the escaping |
| 4434 | // guarantee the whole vocabulary rests on is untouched. |
| 4435 | if matches! |
| 4436 | slot.kind, |
| 4437 | Handover | Ceded |
| 4438 | && let Some = doc.fills.get |
| 4439 | |
| 4440 | out.push_str; |
| 4441 | |
| 4442 | |
| 4443 | // `ae8e8836`. The region's own popover container, for an anchored screen to |
| 4444 | // land in. Emitted for every region rather than for the ones an app anchors |
| 4445 | // to, because nothing in a description says which those are; the cost is one |
| 4446 | // empty element per region and it is paid knowingly. |
| 4447 | // |
| 4448 | // Inside the region for every kind but one, and last, so that an anchored |
| 4449 | // screen draws over what the description put here rather than under it. |
| 4450 | // Inside is what makes it the region's: a fragment replacing this region |
| 4451 | // takes the menu with it, and a menu left open over contents that have since |
| 4452 | // been swapped is the state nobody asked for. |
| 4453 | // |
| 4454 | // A bespoke region is the exception and gets a sibling instead. Decision 4 |
| 4455 | // says that one is a place and nothing else -- the host's markup is the |
| 4456 | // whole of what is between its tags -- and a renderer slipping its own |
| 4457 | // element in there would be the interpretation the decision forbids. The |
| 4458 | // menu then outlives a re-fill of that region, which is the cost of not |
| 4459 | // reaching inside, and it is the smaller of the two. |
| 4460 | let anchor = cratehandle.map; |
| 4461 | let bespoke = matches! |
| 4462 | slot.kind, |
| 4463 | Handover | Ceded |
| 4464 | ; |
| 4465 | if let Some = &anchor |
| 4466 | && !bespoke |
| 4467 | |
| 4468 | anchor_container_html; |
| 4469 | |
| 4470 | |
| 4471 | let _ = write!; |
| 4472 | |
| 4473 | if let Some = &anchor |
| 4474 | && bespoke |
| 4475 | |
| 4476 | anchor_container_html; |
| 4477 | |
| 4478 | |
| 4479 | // The consult wrappers close outside the anchor container for a bespoke |
| 4480 | // region, which is the one case where the container is a sibling. Both are |
| 4481 | // the region's, and the question is the outer of the two: a menu that |
| 4482 | // opened over the region belongs to it, and a recompute is asked about it. |
| 4483 | for _ in &slot.consults |
| 4484 | out.push_str; |
| 4485 | |
| 4486 | |
| 4487 |