Skip to main content

max / makeover-webview

Say where each cell's block landed, for a caller compiling a row cells_html_placed is cells_html_into plus a record of the offsets each column's div occupies. A caller turning a described screen into a template has to know which bytes a given cell produced, and searching the output for the content cannot answer it: a cell's markup may appear twice in one row, and a cell that renders to nothing cannot be searched for at all. The writer is the only source that cannot be wrong, so it says.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-08 19:31 UTC
Signed with PGP, not checked
Commit: 4dca141f2aca7dacf6b6b214cdccbf4aca58e3e1
Parent: 4568ef1
2 files changed, +92 insertions, -1 deletion
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.74.2"
3 + version = "0.75.0"
4 4 edition = "2024"
5 5 # One copy of this renderer per dependency graph, enforced by cargo rather than
6 6 # by remembering. Two versions means the generated stylesheet and the emitted
M src/list.rs +91
@@ -482,7 +482,45 @@
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,6 +533,9 @@
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,6 +543,56 @@
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