| 482 |
482 |
|
/// rendering a table should call: a row is emitted once per row per render, so
|
| 483 |
483 |
|
/// this is where a `String` per cell class is paid for most often.
|
| 484 |
484 |
|
pub fn cells_html_into(columns: &[Column<'_>], cells: &[Cell<'_>], opts: &Emit, out: &mut String) {
|
|
485 |
+ |
emit_cells(columns, cells, opts, out, None);
|
|
486 |
+ |
}
|
|
487 |
+ |
|
|
488 |
+ |
/// A row's cells, saying where each column's block landed.
|
|
489 |
+ |
///
|
|
490 |
+ |
/// Byte-identical to [`cells_html_into`], and it appends one entry to `placed`
|
|
491 |
+ |
/// per column, in column order: the offsets in `out` between which that
|
|
492 |
+ |
/// column's whole `<div>` was written.
|
|
493 |
+ |
///
|
|
494 |
+ |
/// # Who this is for
|
|
495 |
+ |
///
|
|
496 |
+ |
/// A caller compiling a described screen into a template, which has to know
|
|
497 |
+ |
/// which bytes of a row a particular cell produced. Finding that out by
|
|
498 |
+ |
/// searching the output for the content is the thing it exists to avoid: a
|
|
499 |
+ |
/// cell's markup may appear twice in a row, and a cell that renders to nothing
|
|
500 |
+ |
/// cannot be searched for at all. So the writer says where it wrote, which is
|
|
501 |
+ |
/// the only source that cannot be wrong.
|
|
502 |
+ |
///
|
|
503 |
+ |
/// Nothing about the markup changes, and a caller not compiling anything should
|
|
504 |
+ |
/// call [`cells_html_into`] and pay nothing for this.
|
|
505 |
+ |
pub fn cells_html_placed(
|
|
506 |
+ |
columns: &[Column<'_>],
|
|
507 |
+ |
cells: &[Cell<'_>],
|
|
508 |
+ |
opts: &Emit,
|
|
509 |
+ |
out: &mut String,
|
|
510 |
+ |
placed: &mut Vec<core::ops::Range<usize>>,
|
|
511 |
+ |
) {
|
|
512 |
+ |
emit_cells(columns, cells, opts, out, Some(placed));
|
|
513 |
+ |
}
|
|
514 |
+ |
|
|
515 |
+ |
fn emit_cells(
|
|
516 |
+ |
columns: &[Column<'_>],
|
|
517 |
+ |
cells: &[Cell<'_>],
|
|
518 |
+ |
opts: &Emit,
|
|
519 |
+ |
out: &mut String,
|
|
520 |
+ |
mut placed: Option<&mut Vec<core::ops::Range<usize>>>,
|
|
521 |
+ |
) {
|
| 485 |
522 |
|
for column in columns {
|
|
523 |
+ |
let at = out.len();
|
| 486 |
524 |
|
let found = cells.iter().find(|cell| cell.column == column.name);
|
| 487 |
525 |
|
out.push_str("<div class=\"");
|
| 488 |
526 |
|
push_class(out, "cell", opts);
|
| 495 |
533 |
|
out.push_str("\">");
|
| 496 |
534 |
|
out.push_str(found.map_or("", |cell| cell.content.0));
|
| 497 |
535 |
|
out.push_str("</div>");
|
|
536 |
+ |
if let Some(placed) = placed.as_deref_mut() {
|
|
537 |
+ |
placed.push(at..out.len());
|
|
538 |
+ |
}
|
| 498 |
539 |
|
}
|
| 499 |
540 |
|
}
|
| 500 |
541 |
|
|
| 502 |
543 |
|
mod tests {
|
| 503 |
544 |
|
use super::*;
|
| 504 |
545 |
|
|
|
546 |
+ |
/// The placed form writes the same bytes, and says where each one went.
|
|
547 |
+ |
///
|
|
548 |
+ |
/// Both halves matter. If the two ever disagreed, a compiled screen would
|
|
549 |
+ |
/// be built against markup nobody serves; if a range were off by a byte, it
|
|
550 |
+ |
/// would cut a tag in half.
|
|
551 |
+ |
#[test]
|
|
552 |
+ |
fn saying_where_a_cell_landed_does_not_change_what_is_written() {
|
|
553 |
+ |
let columns = [
|
|
554 |
+ |
Column {
|
|
555 |
+ |
name: "Name",
|
|
556 |
+ |
..Column::new("Name")
|
|
557 |
+ |
},
|
|
558 |
+ |
Column {
|
|
559 |
+ |
name: "Price",
|
|
560 |
+ |
..Column::new("Price")
|
|
561 |
+ |
},
|
|
562 |
+ |
];
|
|
563 |
+ |
let cells = [
|
|
564 |
+ |
Cell {
|
|
565 |
+ |
column: "Name",
|
|
566 |
+ |
part: Some(CellPart::Value),
|
|
567 |
+ |
content: Markup("Kick"),
|
|
568 |
+ |
},
|
|
569 |
+ |
Cell {
|
|
570 |
+ |
column: "Price",
|
|
571 |
+ |
part: None,
|
|
572 |
+ |
content: Markup("<b>Free</b>"),
|
|
573 |
+ |
},
|
|
574 |
+ |
];
|
|
575 |
+ |
let opts = Emit::default();
|
|
576 |
+ |
|
|
577 |
+ |
let mut plain = String::new();
|
|
578 |
+ |
cells_html_into(&columns, &cells, &opts, &mut plain);
|
|
579 |
+ |
|
|
580 |
+ |
let mut said = String::from("before:");
|
|
581 |
+ |
let mut placed = Vec::new();
|
|
582 |
+ |
cells_html_placed(&columns, &cells, &opts, &mut said, &mut placed);
|
|
583 |
+ |
|
|
584 |
+ |
assert_eq!(said.strip_prefix("before:").unwrap(), plain);
|
|
585 |
+ |
assert_eq!(placed.len(), columns.len());
|
|
586 |
+ |
assert_eq!(placed[0].start, "before:".len());
|
|
587 |
+ |
assert_eq!(placed[1].end, said.len());
|
|
588 |
+ |
for range in &placed {
|
|
589 |
+ |
let block = &said[range.clone()];
|
|
590 |
+ |
assert!(block.starts_with("<div class=\""), "{block}");
|
|
591 |
+ |
assert!(block.ends_with("</div>"), "{block}");
|
|
592 |
+ |
}
|
|
593 |
+ |
assert!(said[placed[1].clone()].contains("<b>Free</b>"));
|
|
594 |
+ |
}
|
|
595 |
+ |
|
| 505 |
596 |
|
#[test]
|
| 506 |
597 |
|
fn every_width_and_drop_class_is_one_the_vocabulary_wrote_down() {
|
| 507 |
598 |
|
// The obligation ROW_PART_CLASSES carries. Both matches have a wildcard
|