| 32 |
32 |
|
// than from a match here, so a tone added upstream cannot be named two ways.
|
| 33 |
33 |
|
use makeover_layout::Intent as _;
|
| 34 |
34 |
|
use makeover_webview::figure::figure_html;
|
| 35 |
|
- |
use makeover_webview::form::{Filling, Markup, Value, escape, field_html};
|
|
35 |
+ |
use makeover_webview::form::{Filling, Markup, Value, escape_into, field_html};
|
| 36 |
36 |
|
// `class`, `option_class` and the two part-class mappings below are makeover's,
|
| 37 |
37 |
|
// not copies of it. They were copies until makeover-webview 0.27.0 made them
|
| 38 |
38 |
|
// public: the prefix helper was byte-identical, and the row and cell part names
|
| 51 |
51 |
|
use quasi_router::screen::{Act, Cell, Cells, Destination, Field, Node, Row, Slot, Tag};
|
| 52 |
52 |
|
use quasi_router::{Action, Method, Params};
|
| 53 |
53 |
|
|
|
54 |
+ |
/// Write one prefixed class name onto a buffer the caller already has.
|
|
55 |
+ |
///
|
|
56 |
+ |
/// The two halves are escaped separately rather than joined and escaped once.
|
|
57 |
+ |
/// That is the same bytes -- escaping is per character and has no context to
|
|
58 |
+ |
/// carry across the seam -- for none of the allocations. Every class of every
|
|
59 |
+ |
/// element went through a `format!` and then a second `String` before this
|
|
60 |
+ |
/// existed, which measured as most of the emitter's allocation count.
|
|
61 |
+ |
///
|
|
62 |
+ |
/// Escaped at all because a prefix is host configuration reaching an attribute
|
|
63 |
+ |
/// value. It is a `&'static str` and every real one is identity under this, so
|
|
64 |
+ |
/// the cost is a scan; what it buys is that the one string here that did not
|
|
65 |
+ |
/// come from this crate cannot end the attribute.
|
|
66 |
+ |
pub(crate) fn class_into(name: &str, opts: &Emit, out: &mut String) {
|
|
67 |
+ |
escape_into(opts.class_prefix, out);
|
|
68 |
+ |
escape_into(name, out);
|
|
69 |
+ |
}
|
|
70 |
+ |
|
| 54 |
71 |
|
/// Write a `class="..."` attribute, prefixed.
|
| 55 |
72 |
|
fn class_attr(names: &[&str], opts: &Emit, out: &mut String) {
|
| 56 |
73 |
|
out.push_str(" class=\"");
|
| 58 |
75 |
|
if i > 0 {
|
| 59 |
76 |
|
out.push(' ');
|
| 60 |
77 |
|
}
|
| 61 |
|
- |
out.push_str(&escape(&class(name, opts)));
|
|
78 |
+ |
class_into(name, opts, out);
|
| 62 |
79 |
|
}
|
| 63 |
80 |
|
out.push('"');
|
| 64 |
81 |
|
}
|
| 179 |
196 |
|
docengine::render_strict(source)
|
| 180 |
197 |
|
}
|
| 181 |
198 |
|
|
| 182 |
|
- |
/// JSON-encode a string, for an `hx-vals` payload.
|
|
199 |
+ |
/// JSON-encode a string into an attribute value, both rules in one pass.
|
| 183 |
200 |
|
///
|
| 184 |
201 |
|
/// Small enough to own. Pulling in a JSON crate to write object literals of
|
| 185 |
202 |
|
/// strings would be the larger decision, and the encoder a renderer needs is
|
| 186 |
203 |
|
/// this: the six characters JSON requires escaped, plus a `\u00XX` form for the
|
| 187 |
|
- |
/// rest of the C0 range. The result is then HTML-escaped by the caller, because
|
| 188 |
|
- |
/// it lands in an attribute.
|
| 189 |
|
- |
fn json_string(text: &str, out: &mut String) {
|
| 190 |
|
- |
out.push('"');
|
|
204 |
+ |
/// rest of the C0 range.
|
|
205 |
+ |
///
|
|
206 |
+ |
/// The HTML escaping is applied here rather than by the caller, which is what
|
|
207 |
+ |
/// lets the payload go straight into `out`. Building the object and then
|
|
208 |
+ |
/// escaping the whole of it allocated a `String` for each, per action, and
|
|
209 |
+ |
/// there was nothing in between to look at.
|
|
210 |
+ |
///
|
|
211 |
+ |
/// The two rules compose in this order and only this order. JSON runs first, so
|
|
212 |
+ |
/// a quote in the text becomes `\"` and then `\"`; the backslash JSON adds
|
|
213 |
+ |
/// is not a character HTML encodes, and the quote HTML encodes is not one JSON
|
|
214 |
+ |
/// would look at twice. The structural quotes the object needs are written as
|
|
215 |
+ |
/// `"` directly, because they are markup rather than content.
|
|
216 |
+ |
fn json_string_attr(text: &str, out: &mut String) {
|
|
217 |
+ |
out.push_str(""");
|
| 191 |
218 |
|
for ch in text.chars() {
|
| 192 |
219 |
|
match ch {
|
| 193 |
|
- |
'"' => out.push_str("\\\""),
|
|
220 |
+ |
// JSON first, then the HTML form of what it produced.
|
|
221 |
+ |
'"' => out.push_str("\\""),
|
| 194 |
222 |
|
'\\' => out.push_str("\\\\"),
|
| 195 |
223 |
|
'\n' => out.push_str("\\n"),
|
| 196 |
224 |
|
'\r' => out.push_str("\\r"),
|
| 198 |
226 |
|
c if (c as u32) < 0x20 => {
|
| 199 |
227 |
|
let _ = write!(out, "\\u{:04x}", c as u32);
|
| 200 |
228 |
|
}
|
|
229 |
+ |
// The rest of what an attribute value cannot carry. JSON has no
|
|
230 |
+ |
// opinion on any of these, so this arm is HTML's alone and matches
|
|
231 |
+ |
// `escape_into` character for character.
|
|
232 |
+ |
'&' => out.push_str("&"),
|
|
233 |
+ |
'<' => out.push_str("<"),
|
|
234 |
+ |
'>' => out.push_str(">"),
|
|
235 |
+ |
'\'' => out.push_str("'"),
|
| 201 |
236 |
|
c => out.push(c),
|
| 202 |
237 |
|
}
|
| 203 |
238 |
|
}
|
| 204 |
|
- |
out.push('"');
|
|
239 |
+ |
out.push_str(""");
|
| 205 |
240 |
|
}
|
| 206 |
241 |
|
|
| 207 |
|
- |
/// The params as an `hx-vals` object.
|
| 208 |
|
- |
fn json_object(params: &Params) -> String {
|
| 209 |
|
- |
let mut json = String::from("{");
|
|
242 |
+ |
/// The params as an `hx-vals` object, written into the attribute they land in.
|
|
243 |
+ |
fn json_object_attr(params: &Params, out: &mut String) {
|
|
244 |
+ |
out.push('{');
|
| 210 |
245 |
|
for (i, (name, value)) in params.iter().enumerate() {
|
| 211 |
246 |
|
if i > 0 {
|
| 212 |
|
- |
json.push(',');
|
|
247 |
+ |
out.push(',');
|
| 213 |
248 |
|
}
|
| 214 |
|
- |
json_string(name, &mut json);
|
| 215 |
|
- |
json.push(':');
|
| 216 |
|
- |
json_string(value, &mut json);
|
|
249 |
+ |
json_string_attr(name, out);
|
|
250 |
+ |
out.push(':');
|
|
251 |
+ |
json_string_attr(value, out);
|
| 217 |
252 |
|
}
|
| 218 |
|
- |
json.push('}');
|
| 219 |
|
- |
json
|
|
253 |
+ |
out.push('}');
|
| 220 |
254 |
|
}
|
| 221 |
255 |
|
|
| 222 |
256 |
|
/// What makes a control call its action.
|
| 292 |
326 |
|
// handle on this one.
|
| 293 |
327 |
|
if let Destination::External(url) = &action.destination {
|
| 294 |
328 |
|
out.push_str(" href=\"");
|
| 295 |
|
- |
out.push_str(&escape(url));
|
|
329 |
+ |
escape_into(url, out);
|
| 296 |
330 |
|
out.push_str("\" target=\"_blank\" rel=\"noopener noreferrer\"");
|
| 297 |
331 |
|
return;
|
| 298 |
332 |
|
}
|
| 299 |
333 |
|
|
|
334 |
+ |
// The address, built once. The `href` below and the verb further down are
|
|
335 |
+ |
// the same string whenever both are emitted -- a read of a route names one
|
|
336 |
+ |
// place -- and building it twice was a `String` per link on every screen
|
|
337 |
+ |
// made mostly of links.
|
|
338 |
+ |
let url = quasi_http::route_url(action.destination.as_str(), &action.carried);
|
|
339 |
+ |
|
| 300 |
340 |
|
// A read of a route this app answers is a link, and it gets the address as
|
| 301 |
341 |
|
// well as the transport. htmx uses `hx-get` and prevents the default, so
|
| 302 |
342 |
|
// the `href` is what everything else uses: middle-click, copy-link, a
|
| 303 |
343 |
|
// crawler, and the page with JS off. The parameters are folded into it
|
| 304 |
344 |
|
// because a link to a filtered list that drops the filter is a different
|
| 305 |
345 |
|
// place, and `hx-vals` below carries the same ones down htmx's path.
|
| 306 |
|
- |
if let Destination::Route(path) = &action.destination
|
| 307 |
|
- |
&& !action.method.mutates()
|
| 308 |
|
- |
{
|
|
346 |
+ |
if matches!(action.destination, Destination::Route(_)) && !action.method.mutates() {
|
| 309 |
347 |
|
out.push_str(" href=\"");
|
| 310 |
|
- |
out.push_str(&escape(&quasi_http::route_url(path, &action.carried)));
|
|
348 |
+ |
escape_into(&url, out);
|
| 311 |
349 |
|
out.push('"');
|
| 312 |
350 |
|
}
|
| 313 |
351 |
|
|
| 321 |
359 |
|
// crate anywhere else is what the architectural test forbids.
|
| 322 |
360 |
|
if let Some(selector) = gathers {
|
| 323 |
361 |
|
out.push_str(" hx-include=\"");
|
| 324 |
|
- |
out.push_str(&escape(selector));
|
|
362 |
+ |
escape_into(selector, out);
|
| 325 |
363 |
|
out.push('"');
|
| 326 |
364 |
|
}
|
| 327 |
365 |
|
|
| 330 |
368 |
|
// about: where it lands, then what is sent.
|
| 331 |
369 |
|
if let Some(region) = &action.replaces {
|
| 332 |
370 |
|
out.push_str(" hx-target=\"#");
|
| 333 |
|
- |
out.push_str(&escape(region));
|
|
371 |
+ |
escape_into(region, out);
|
| 334 |
372 |
|
out.push('"');
|
| 335 |
373 |
|
}
|
| 336 |
374 |
|
|
| 347 |
385 |
|
} else {
|
| 348 |
386 |
|
out.push_str(" data-saves=\"");
|
| 349 |
387 |
|
}
|
| 350 |
|
- |
out.push_str(&escape(filename));
|
|
388 |
+ |
escape_into(filename, out);
|
| 351 |
389 |
|
out.push('"');
|
| 352 |
390 |
|
}
|
| 353 |
391 |
|
|
| 358 |
396 |
|
Method::Put => " hx-put=\"",
|
| 359 |
397 |
|
};
|
| 360 |
398 |
|
out.push_str(verb);
|
| 361 |
|
- |
out.push_str(&escape(&quasi_http::route_url(
|
| 362 |
|
- |
action.destination.as_str(),
|
| 363 |
|
- |
&action.carried,
|
| 364 |
|
- |
)));
|
|
399 |
+ |
escape_into(&url, out);
|
| 365 |
400 |
|
out.push('"');
|
| 366 |
401 |
|
|
| 367 |
402 |
|
if !action.params.is_empty() {
|
| 368 |
403 |
|
out.push_str(" hx-vals=\"");
|
| 369 |
|
- |
out.push_str(&escape(&json_object(&action.params)));
|
|
404 |
+ |
json_object_attr(&action.params, out);
|
| 370 |
405 |
|
out.push('"');
|
| 371 |
406 |
|
}
|
| 372 |
407 |
|
|
| 387 |
422 |
|
// trigger on itself would wait for a keystroke it can never
|
| 388 |
423 |
|
// receive.
|
| 389 |
424 |
|
out.push_str(" hx-trigger=\"keydown[");
|
| 390 |
|
- |
out.push_str(&escape(filter));
|
|
425 |
+ |
escape_into(filter, out);
|
| 391 |
426 |
|
out.push_str("] from:body\"");
|
| 392 |
427 |
|
}
|
| 393 |
428 |
|
Fires::ChangeInside => {
|
| 402 |
437 |
|
// place, or swapping htmx for fixi stops being a one-function change.
|
| 403 |
438 |
|
if let Some(prompt) = confirm {
|
| 404 |
439 |
|
out.push_str(" hx-confirm=\"");
|
| 405 |
|
- |
out.push_str(&escape(prompt));
|
|
440 |
+ |
escape_into(prompt, out);
|
| 406 |
441 |
|
out.push('"');
|
| 407 |
442 |
|
}
|
| 408 |
443 |
|
|
| 453 |
488 |
|
// have in common. Built here because it needs `Emit`, which the transport
|
| 454 |
489 |
|
// function does not take -- a host setting `class_prefix` moves the class
|
| 455 |
490 |
|
// and the selector together.
|
| 456 |
|
- |
let gathered = class("row-select", opts);
|
| 457 |
|
- |
let gathers = act.over.as_ref().map(|_| format!(".{gathered}"));
|
|
491 |
+ |
//
|
|
492 |
+ |
// Built inside the `map` and not before it: staged selections are rare and
|
|
493 |
+ |
// every other control on the screen was paying for two `String`s it then
|
|
494 |
+ |
// dropped. `class` rather than `class_into` because what is wanted here is
|
|
495 |
+ |
// a value, and a selector is the one place this crate holds one.
|
|
496 |
+ |
let gathers = act
|
|
497 |
+ |
.over
|
|
498 |
+ |
.as_ref()
|
|
499 |
+ |
.map(|_| format!(".{}", class("row-select", opts)));
|
| 458 |
500 |
|
let gathers = gathers.as_deref();
|
| 459 |
501 |
|
// `button`, which is makeover's name for this and carries its whole
|
| 460 |
502 |
|
// interactive set: the raised bevel, the hover fill, the pressed inset, the
|
| 513 |
555 |
|
// terminal and this is the nearest honest thing a page has.
|
| 514 |
556 |
|
if let Some(key) = &act.key {
|
| 515 |
557 |
|
out.push_str(" accesskey=\"");
|
| 516 |
|
- |
out.push_str(&escape(key));
|
|
558 |
+ |
escape_into(key, out);
|
| 517 |
559 |
|
out.push('"');
|
| 518 |
560 |
|
}
|
| 519 |
561 |
|
|
| 520 |
562 |
|
out.push('>');
|
| 521 |
|
- |
out.push_str(&escape(&act.label));
|
|
563 |
+ |
escape_into(&act.label, out);
|
| 522 |
564 |
|
out.push_str(close);
|
| 523 |
565 |
|
}
|
| 524 |
566 |
|
|
| 562 |
604 |
|
// which one.
|
| 563 |
605 |
|
if let Some(value) = &row.value {
|
| 564 |
606 |
|
out.push_str(" name=\"");
|
| 565 |
|
- |
out.push_str(&escape(quasi_router::Node::TICKED));
|
|
607 |
+ |
escape_into(quasi_router::Node::TICKED, out);
|
| 566 |
608 |
|
out.push_str("\" value=\"");
|
| 567 |
|
- |
out.push_str(&escape(value));
|
|
609 |
+ |
escape_into(value, out);
|
| 568 |
610 |
|
out.push('"');
|
| 569 |
611 |
|
}
|
| 570 |
612 |
|
// A tick with no route is local state until something submits it, which
|
| 673 |
715 |
|
/// which to do.
|
| 674 |
716 |
|
fn row_inline_html(node: &Node, morphs: bool, opts: &Emit, out: &mut String) {
|
| 675 |
717 |
|
match node {
|
| 676 |
|
- |
Node::Text { text, .. } => out.push_str(&escape(text)),
|
|
718 |
+ |
Node::Text { text, .. } => escape_into(text, out),
|
| 677 |
719 |
|
Node::Rich { source } => out.push_str(&docengine::render_phrase(source)),
|
| 678 |
720 |
|
Node::Token(tag) => tag_html(tag, morphs, opts, out),
|
| 679 |
721 |
|
Node::Act(act) => act_html(act, morphs, opts, out),
|
| 702 |
744 |
|
/// `cell-tokens` and `cell-actions` carry the gap between siblings, so a span
|
| 703 |
745 |
|
/// each would space them as though they were unrelated, and the common case --
|
| 704 |
746 |
|
/// a status column of three badges -- is exactly the consecutive one.
|
| 705 |
|
- |
fn cell_run_html(cell: &Cell, morphs: bool, opts: &Emit) -> String {
|
|
747 |
+ |
///
|
|
748 |
+ |
/// Writes into a buffer the caller owns rather than answering with one. The
|
|
749 |
+ |
/// caller is [`cells_row_html`], which needs every cell of a row to exist at
|
|
750 |
+ |
/// once because `cells_html` takes them together -- but needs that only within
|
|
751 |
+ |
/// the row, so the buffers are reused down the table and a fresh `String` per
|
|
752 |
+ |
/// cell per row was the emitter's largest remaining cost.
|
|
753 |
+ |
fn cell_run_html(cell: &Cell, morphs: bool, opts: &Emit, out: &mut String) {
|
| 706 |
754 |
|
// A cell that is one piece of text says so on the container through
|
| 707 |
755 |
|
// `CellPart::Value`, so a wrapper span here would say nothing the container
|
| 708 |
756 |
|
// has not. Anything else names its parts inside, or the content colour on
|
| 710 |
758 |
|
// makeover-layout 0.14.0 named and makeover-webview 0.25.0 stopped
|
| 711 |
759 |
|
// emitting.
|
| 712 |
760 |
|
if let [Node::Text { text, .. }] = cell.parts.as_slice() {
|
| 713 |
|
- |
return escape(text);
|
|
761 |
+ |
escape_into(text, out);
|
|
762 |
+ |
return;
|
| 714 |
763 |
|
}
|
| 715 |
764 |
|
|
| 716 |
|
- |
let mut out = String::new();
|
| 717 |
765 |
|
let mut rest = cell.parts.as_slice();
|
| 718 |
766 |
|
while let Some((head, tail)) = rest.split_first() {
|
| 719 |
767 |
|
match head {
|
| 720 |
768 |
|
Node::Text { text, .. } => {
|
| 721 |
769 |
|
out.push_str("<span");
|
| 722 |
|
- |
class_attr(&[cell_part_class(layout::CellPart::Value)], opts, &mut out);
|
|
770 |
+ |
class_attr(&[cell_part_class(layout::CellPart::Value)], opts, out);
|
| 723 |
771 |
|
out.push('>');
|
| 724 |
|
- |
out.push_str(&escape(text));
|
|
772 |
+ |
escape_into(text, out);
|
| 725 |
773 |
|
out.push_str("</span>");
|
| 726 |
774 |
|
rest = tail;
|
| 727 |
775 |
|
}
|
| 736 |
784 |
|
Node::Link { text, action } => {
|
| 737 |
785 |
|
let (open, close) = control_tag(action);
|
| 738 |
786 |
|
out.push_str(open);
|
| 739 |
|
- |
class_attr(&[cell_part_class(layout::CellPart::Link)], opts, &mut out);
|
|
787 |
+ |
class_attr(&[cell_part_class(layout::CellPart::Link)], opts, out);
|
| 740 |
788 |
|
out.push_str(" data-act");
|
| 741 |
|
- |
action_attrs(action, Fires::Click, None, morphs, None, &mut out);
|
|
789 |
+ |
action_attrs(action, Fires::Click, None, morphs, None, out);
|
| 742 |
790 |
|
out.push('>');
|
| 743 |
|
- |
out.push_str(&escape(text));
|
|
791 |
+ |
escape_into(text, out);
|
| 744 |
792 |
|
out.push_str(close);
|
| 745 |
793 |
|
rest = tail;
|
| 746 |
794 |
|
}
|
| 747 |
795 |
|
Node::Token(_) => {
|
| 748 |
796 |
|
let run = rest.iter().take_while(|p| matches!(p, Node::Token(_)));
|
| 749 |
797 |
|
out.push_str("<span");
|
| 750 |
|
- |
class_attr(&[cell_part_class(layout::CellPart::Tokens)], opts, &mut out);
|
|
798 |
+ |
class_attr(&[cell_part_class(layout::CellPart::Tokens)], opts, out);
|
| 751 |
799 |
|
out.push('>');
|
| 752 |
800 |
|
let mut taken = 0;
|
| 753 |
801 |
|
for part in run {
|
| 754 |
802 |
|
if let Node::Token(tag) = part {
|
| 755 |
|
- |
tag_html(tag, morphs, opts, &mut out);
|
|
803 |
+ |
tag_html(tag, morphs, opts, out);
|
| 756 |
804 |
|
}
|
| 757 |
805 |
|
taken += 1;
|
| 758 |
806 |
|
}
|
| 766 |
814 |
|
Node::Act(_) => {
|
| 767 |
815 |
|
let run = rest.iter().take_while(|p| matches!(p, Node::Act(_)));
|
| 768 |
816 |
|
out.push_str("<span");
|
| 769 |
|
- |
class_attr(
|
| 770 |
|
- |
&[cell_part_class(layout::CellPart::Actions)],
|
| 771 |
|
- |
opts,
|
| 772 |
|
- |
&mut out,
|
| 773 |
|
- |
);
|
|
817 |
+ |
class_attr(&[cell_part_class(layout::CellPart::Actions)], opts, out);
|
| 774 |
818 |
|
out.push('>');
|
| 775 |
819 |
|
let mut taken = 0;
|
| 776 |
820 |
|
for part in run {
|
| 777 |
821 |
|
if let Node::Act(act) = part {
|
| 778 |
|
- |
act_html(act, morphs, opts, &mut out);
|
|
822 |
+ |
act_html(act, morphs, opts, out);
|
| 779 |
823 |
|
}
|
| 780 |
824 |
|
taken += 1;
|
| 781 |
825 |
|
}
|
| 789 |
833 |
|
other => {
|
| 790 |
834 |
|
// No fills: a bespoke region is a block and cannot reach a run,
|
| 791 |
835 |
|
// which the containment bound is what guarantees.
|
| 792 |
|
- |
node_html(other, morphs, opts, &HashMap::new(), &mut out);
|
|
836 |
+ |
node_html(other, morphs, opts, &HashMap::new(), out);
|
| 793 |
837 |
|
rest = tail;
|
| 794 |
838 |
|
}
|
| 795 |
839 |
|
}
|
| 796 |
840 |
|
}
|
| 797 |
|
- |
out
|
|
841 |
+ |
}
|
|
842 |
+ |
|
|
843 |
+ |
/// The buffers a table's rows take turns in.
|
|
844 |
+ |
///
|
|
845 |
+ |
/// `cells_html` takes a row's cells together, so the markup of every cell in one
|
|
846 |
+ |
/// row has to exist at once. Nothing says it has to be new: cleared and
|
|
847 |
+ |
/// refilled, these keep the capacity the first row bought, and a table's second
|
|
848 |
+ |
/// row onwards writes into memory that already exists.
|
|
849 |
+ |
///
|
|
850 |
+ |
/// [`Emitted`] is not here, because it borrows from `filled` and a struct
|
|
851 |
+ |
/// holding both would be self-referential. It is a `Vec` per row and stays one.
|
|
852 |
+ |
#[derive(Default)]
|
|
853 |
+ |
struct RowBuffers {
|
|
854 |
+ |
/// One cell's markup each, in column order.
|
|
855 |
+ |
filled: Vec<String>,
|
|
856 |
+ |
/// Which cells are a single piece of text, which is what earns the
|
|
857 |
+ |
/// container the part class.
|
|
858 |
+ |
parts: Vec<Option<layout::CellPart>>,
|
| 798 |
859 |
|
}
|
| 799 |
860 |
|
|
| 800 |
861 |
|
/// One row of a table.
|
|
862 |
+ |
///
|
|
863 |
+ |
/// Takes the columns already borrowed rather than the described ones. They are
|
|
864 |
+ |
/// the same for every row of the table, and this built the borrowed list again
|
|
865 |
+ |
/// per row until the emitter's allocations were counted.
|
| 801 |
866 |
|
fn cells_row_html(
|
| 802 |
867 |
|
cells: &Cells,
|
| 803 |
|
- |
columns: &[quasi_router::screen::Column],
|
|
868 |
+ |
columns: &[layout::Column<'_>],
|
|
869 |
+ |
buffers: &mut RowBuffers,
|
| 804 |
870 |
|
morphs: bool,
|
| 805 |
871 |
|
opts: &Emit,
|
| 806 |
872 |
|
out: &mut String,
|
| 838 |
904 |
|
// goes in. Ours is text from a description plus, since `022f0c59`, whatever
|
| 839 |
905 |
|
// controls the cell carries, and `cells_html` is what knows the column
|
| 840 |
906 |
|
// classes and the narrowing.
|
| 841 |
|
- |
let filled: Vec<String> = cells
|
| 842 |
|
- |
.values
|
| 843 |
|
- |
.iter()
|
| 844 |
|
- |
.map(|cell| cell_run_html(cell, morphs, opts))
|
| 845 |
|
- |
.collect();
|
|
907 |
+ |
let RowBuffers { filled, parts } = buffers;
|
|
908 |
+ |
filled.truncate(cells.values.len());
|
|
909 |
+ |
for (at, cell) in cells.values.iter().enumerate() {
|
|
910 |
+ |
match filled.get_mut(at) {
|
|
911 |
+ |
Some(buffer) => buffer.clear(),
|
|
912 |
+ |
None => filled.push(String::new()),
|
|
913 |
+ |
}
|
|
914 |
+ |
cell_run_html(cell, morphs, opts, &mut filled[at]);
|
|
915 |
+ |
}
|
| 846 |
916 |
|
// The container says what the cell is only when the cell is nothing but one
|
| 847 |
917 |
|
// piece of text, which is the case where a wrapper span would say nothing
|
| 848 |
918 |
|
// the container has not already said. Anything else names its parts inside
|
| 849 |
919 |
|
// -- the anchor is a `cell-link`, the strips are `cell-tokens` and
|
| 850 |
920 |
|
// `cell-actions` -- because a colour on the container would reach all of
|
| 851 |
921 |
|
// them, and that is the drift makeover-layout 0.14.0 named.
|
| 852 |
|
- |
let parts: Vec<Option<layout::CellPart>> = cells
|
| 853 |
|
- |
.values
|
| 854 |
|
- |
.iter()
|
| 855 |
|
- |
.map(|cell| {
|
| 856 |
|
- |
matches!(cell.parts.as_slice(), [Node::Text { .. }]).then_some(layout::CellPart::Value)
|
| 857 |
|
- |
})
|
| 858 |
|
- |
.collect();
|
| 859 |
|
- |
let borrowed: Vec<layout::Column<'_>> = columns
|
| 860 |
|
- |
.iter()
|
| 861 |
|
- |
.map(quasi_router::screen::Column::as_layout)
|
| 862 |
|
- |
.collect();
|
| 863 |
|
- |
let cells: Vec<Emitted<'_>> = borrowed
|
|
922 |
+ |
parts.clear();
|
|
923 |
+ |
parts.extend(cells.values.iter().map(|cell| {
|
|
924 |
+ |
matches!(cell.parts.as_slice(), [Node::Text { .. }]).then_some(layout::CellPart::Value)
|
|
925 |
+ |
}));
|
|
926 |
+ |
let emitted: Vec<Emitted<'_>> = columns
|
| 864 |
927 |
|
.iter()
|
| 865 |
928 |
|
.zip(filled.iter())
|
| 866 |
|
- |
.zip(parts)
|
|
929 |
+ |
.zip(parts.iter().copied())
|
| 867 |
930 |
|
.map(|((column, value), part)| Emitted {
|
| 868 |
931 |
|
column: column.name,
|
| 869 |
932 |
|
part,
|
| 870 |
933 |
|
content: Markup(value),
|
| 871 |
934 |
|
})
|
| 872 |
935 |
|
.collect();
|
| 873 |
|
- |
out.push_str(&cells_html(&borrowed, &cells, opts));
|
|
936 |
+ |
out.push_str(&cells_html(columns, &emitted, opts));
|
| 874 |
937 |
|
|
| 875 |
938 |
|
out.push_str("</div>");
|
| 876 |
939 |
|
}
|
| 890 |
953 |
|
layout::Heading::Section => "h2",
|
| 891 |
954 |
|
layout::Heading::Subsection => "h3",
|
| 892 |
955 |
|
};
|
| 893 |
|
- |
let _ = write!(out, "<{tag}");
|
|
956 |
+ |
out.push('<');
|
|
957 |
+ |
out.push_str(tag);
|
| 894 |
958 |
|
class_attr(&["heading"], opts, out);
|
| 895 |
959 |
|
out.push('>');
|
| 896 |
|
- |
out.push_str(&escape(text));
|
| 897 |
|
- |
let _ = write!(out, "</{tag}>");
|
|
960 |
+ |
escape_into(text, out);
|
|
961 |
+ |
out.push_str("</");
|
|
962 |
+ |
out.push_str(tag);
|
|
963 |
+ |
out.push('>');
|
| 898 |
964 |
|
}
|
| 899 |
965 |
|
|
| 900 |
966 |
|
Node::Text { text, tone } => {
|
| 902 |
968 |
|
class_attr(&["text"], opts, out);
|
| 903 |
969 |
|
tone_attr(*tone, out);
|
| 904 |
970 |
|
out.push('>');
|
| 905 |
|
- |
out.push_str(&escape(text));
|
|
971 |
+ |
escape_into(text, out);
|
| 906 |
972 |
|
out.push_str("</p>");
|
| 907 |
973 |
|
}
|
| 908 |
974 |
|
|
| 947 |
1013 |
|
out.push_str(" data-act");
|
| 948 |
1014 |
|
action_attrs(action, Fires::Click, None, morphs, None, out);
|
| 949 |
1015 |
|
out.push('>');
|
| 950 |
|
- |
out.push_str(&escape(text));
|
|
1016 |
+ |
escape_into(text, out);
|
| 951 |
1017 |
|
out.push_str(close);
|
| 952 |
1018 |
|
}
|
| 953 |
1019 |
|
|
| 979 |
1045 |
|
// what stops an attribute breaking out, and unlike `href` an `src`
|
| 980 |
1046 |
|
// has no scheme that executes -- `javascript:` in an `<img src>` is
|
| 981 |
1047 |
|
// a broken picture, not a script.
|
| 982 |
|
- |
out.push_str(&escape(&picture.src));
|
|
1048 |
+ |
escape_into(&picture.src, out);
|
| 983 |
1049 |
|
out.push_str("\" alt=\"");
|
| 984 |
|
- |
out.push_str(&escape(&picture.alt));
|
|
1050 |
+ |
escape_into(&picture.alt, out);
|
| 985 |
1051 |
|
out.push('"');
|
| 986 |
1052 |
|
|
| 987 |
1053 |
|
// The picture's own dimensions, which is how the browser holds its
|
| 1009 |
1075 |
|
out.push_str("<figcaption");
|
| 1010 |
1076 |
|
class_attr(&["picture-caption"], opts, out);
|
| 1011 |
1077 |
|
out.push('>');
|
| 1012 |
|
- |
out.push_str(&escape(caption));
|
|
1078 |
+ |
escape_into(caption, out);
|
| 1013 |
1079 |
|
out.push_str("</figcaption></figure>");
|
| 1014 |
1080 |
|
}
|
| 1015 |
1081 |
|
}
|
| 1035 |
1101 |
|
out.push_str(" role=\"status\" aria-live=\"polite\"");
|
| 1036 |
1102 |
|
}
|
| 1037 |
1103 |
|
out.push('>');
|
| 1038 |
|
- |
out.push_str(&escape(text));
|
|
1104 |
+ |
escape_into(text, out);
|
| 1039 |
1105 |
|
out.push_str("</div>");
|
| 1040 |
1106 |
|
}
|
| 1041 |
1107 |
|
|
| 1056 |
1122 |
|
out.push_str("<button type=\"submit\"");
|
| 1057 |
1123 |
|
class_attr(&["button", "act-submit"], opts, out);
|
| 1058 |
1124 |
|
out.push('>');
|
| 1059 |
|
- |
out.push_str(&escape(submit));
|
|
1125 |
+ |
escape_into(submit, out);
|
| 1060 |
1126 |
|
out.push_str("</button></form>");
|
| 1061 |
1127 |
|
}
|