Skip to main content

max / makeover-webview

Split a cell's placement into the block and what the cell wrote The whole `<div>` was the wrong answer for half the callers, and MNW's own residual test is what said so: a guarded cell compiled to a branch over the block, and turning it off deleted a column the row still draws. A column emits its block whether or not a cell answers it. So the block is what a caller replacing one cell with another covers, and the content is what a caller saying "this cell is there or is not" covers. Reporting one range made those two indistinguishable.
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 21:46 UTC
Signed with PGP, not checked
Commit: 3684111d6bc79105aeee90934d7de8cc07240062
Parent: 242bb58
2 files changed, +56 insertions, -9 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.76.0"
3 + version = "0.77.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 +55 -8
@@ -485,6 +485,27 @@
485 485 emit_cells(columns, cells, opts, out, None);
486 486 }
487 487
488 + /// Where one column's markup went, and which of it the cell wrote.
489 + ///
490 + /// Two ranges because two different things vary, and a caller compiling this
491 + /// into a template needs to say which it means.
492 + ///
493 + /// `block` is the whole `<div>`: the column emits one per column whether or not
494 + /// a cell answers it, so a caller replacing a cell with a different one replaces
495 + /// the block, class and all.
496 + ///
497 + /// `content` is what the cell itself contributed. A cell that is absent leaves
498 + /// the block standing and empty, so a caller compiling "this cell is there or
499 + /// is not" must cover the content alone -- covering the block would delete a
500 + /// `<div>` the row still draws, and the row would lose a column.
501 + #[derive(Clone, Debug, PartialEq, Eq)]
502 + pub struct Placed {
503 + /// The whole column block, opening tag to closing tag.
504 + pub block: core::ops::Range<usize>,
505 + /// What the cell wrote inside it, empty range and all.
506 + pub content: core::ops::Range<usize>,
507 + }
508 +
488 509 /// A row's cells, saying where each column's block landed.
489 510 ///
490 511 /// Byte-identical to [`cells_html_into`], and it appends one entry to `placed`
@@ -507,7 +528,7 @@
507 528 cells: &[Cell<'_>],
508 529 opts: &Emit,
509 530 out: &mut String,
510 - placed: &mut Vec<core::ops::Range<usize>>,
531 + placed: &mut Vec<Placed>,
511 532 ) {
512 533 emit_cells(columns, cells, opts, out, Some(placed));
513 534 }
@@ -517,7 +538,7 @@
517 538 cells: &[Cell<'_>],
518 539 opts: &Emit,
519 540 out: &mut String,
520 - mut placed: Option<&mut Vec<core::ops::Range<usize>>>,
541 + mut placed: Option<&mut Vec<Placed>>,
521 542 ) {
522 543 for column in columns {
523 544 let at = out.len();
@@ -531,10 +552,15 @@
531 552 push_class(out, cell_part_class(part), opts);
532 553 }
533 554 out.push_str("\">");
555 + let content = out.len();
534 556 out.push_str(found.map_or("", |cell| cell.content.0));
557 + let wrote = content..out.len();
535 558 out.push_str("</div>");
536 559 if let Some(placed) = placed.as_deref_mut() {
537 - placed.push(at..out.len());
560 + placed.push(Placed {
561 + block: at..out.len(),
562 + content: wrote,
563 + });
538 564 }
539 565 }
540 566 }
@@ -583,14 +609,35 @@
583 609
584 610 assert_eq!(said.strip_prefix("before:").unwrap(), plain);
585 611 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()];
612 + assert_eq!(placed[0].block.start, "before:".len());
613 + assert_eq!(placed[1].block.end, said.len());
614 + for one in &placed {
615 + let block = &said[one.block.clone()];
590 616 assert!(block.starts_with("<div class=\""), "{block}");
591 617 assert!(block.ends_with("</div>"), "{block}");
592 618 }
593 - assert!(said[placed[1].clone()].contains("<b>Free</b>"));
619 +
620 + // The content is what the cell wrote and nothing the column wrote, so
621 + // it is the half a caller covers when the cell may be absent: the
622 + // block stands either way.
623 + assert_eq!(&said[placed[0].content.clone()], "Kick");
624 + assert_eq!(&said[placed[1].content.clone()], "<b>Free</b>");
625 + for one in &placed {
626 + assert!(one.block.start < one.content.start);
627 + assert!(one.content.end < one.block.end);
628 + }
629 +
630 + // A column no cell answers still draws its block, and the content it
631 + // reports is the empty range inside it. That is the case the whole
632 + // split exists for.
633 + let mut none = String::new();
634 + let mut empty = Vec::new();
635 + cells_html_placed(&columns, &[], &opts, &mut none, &mut empty);
636 + assert_eq!(empty.len(), columns.len());
637 + for one in &empty {
638 + assert!(one.content.is_empty(), "{one:?}");
639 + assert!(!none[one.block.clone()].is_empty());
640 + }
594 641 }
595 642
596 643 #[test]