You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(cli): prevent UTF-8 panic in explore table wrapping (#52)
`devtrail explore` panicked with `byte index X is not a char boundary`
when rendering Markdown tables whose cells contained multi-byte UTF-8
characters such as em-dash (U+2014, 3 bytes), CJK ideograms, accented
characters or emoji. The cause was `wrap_cell_text()` slicing `&str`
by a byte offset derived from terminal columns.
Fix:
- Rewrite `wrap_cell_text()` to iterate with `char_indices()` so every
slice boundary is a valid char boundary.
- Measure text width with `unicode-width` (added as a direct dep under
the existing `tui` feature; was already a transitive dep of
`ratatui`, so no new compile cost) so wrapping and natural column
widths match the visual columns ratatui reserves at render time.
- Manually pad cells by visual width in `render_table_row()` instead
of `format!("{:<width$}", ...)`, which counts chars, not columns —
this also aligns borders for CJK / double-wide content (relevant for
the zh-CN docs the project ships).
- Add 16 unit tests, including a regression for the exact panic
(`em_dash_no_panic`) and an end-to-end pipeline test using the
literal row that crashed the reported document.
Bump CLI to 3.2.3.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+7Lines changed: 7 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,13 @@ and this project uses [independent versioning](README.md#versioning) for Framewo
7
7
8
8
---
9
9
10
+
## CLI 3.2.3 — UTF-8 Crash Fix in `explore` Tables
11
+
12
+
### Fixed (CLI)
13
+
- Fix panic in `devtrail explore` when rendering Markdown tables whose cells contain multi-byte UTF-8 characters (em-dash `—`, CJK ideograms, accented characters, emoji). Cell wrapping now uses `char_indices()` for safe slicing and measures text in visual columns via `unicode-width`, so table borders also stay aligned with Chinese and double-wide content.
let out = wrap_cell_text("alpha beta gamma delta",6);
650
+
for line in&out {
651
+
assert!(!line.starts_with(' '));
652
+
assert!(!line.ends_with(' '));
653
+
}
654
+
}
655
+
656
+
#[test]
657
+
fnwidth_one_with_cjk_terminates(){
658
+
// A width-2 ideogram into width=1: guarantees forward progress by
659
+
// force-consuming one char per iteration. Must not loop forever.
660
+
let out = wrap_cell_text("数据",1);
661
+
assert_eq!(out.len(),2);
662
+
}
663
+
664
+
#[test]
665
+
fnnatural_widths_measure_visual(){
666
+
let header:Vec<String> = vec!["数据".to_string()];
667
+
let body:Vec<Vec<String>> = vec![];
668
+
// Large available width so we return natural widths directly.
669
+
let widths = compute_column_widths(&header,&body,100);
670
+
assert_eq!(widths.len(),1);
671
+
// "数据" has visual width 4; minimum clamp is 3, so result is 4.
672
+
assert_eq!(widths[0],4);
673
+
}
674
+
675
+
#[test]
676
+
fncjk_fits_without_scaling(){
677
+
let header:Vec<String> = vec!["列1".to_string(),"列2".to_string()];
678
+
let body:Vec<Vec<String>> = vec![vec!["数据".to_string(),"テスト".to_string()]];
679
+
let widths = compute_column_widths(&header,&body,100);
680
+
assert_eq!(widths.len(),2);
681
+
// Col0: max of "列1" (3) and "数据" (4) = 4.
682
+
assert_eq!(widths[0],4);
683
+
// Col1: max of "列2" (3) and "テスト" (6) = 6.
684
+
assert_eq!(widths[1],6);
685
+
}
686
+
687
+
/// End-to-end regression: the exact table row that crashed
688
+
/// `devtrail explore` must render through the full pipeline
689
+
/// (parser + renderer + cell wrapping) without panicking.
690
+
#[test]
691
+
fnfull_pipeline_em_dash_table_no_panic(){
692
+
let md = "\
693
+
| Risk | Prob | Impact | Score | Mitigation |
694
+
|------|------|--------|-------|------------|
695
+
| E-003 | 2 | 3 | 6 | Admin/SuperAdmin role required. RLS middleware adds tenant isolation at DB layer. **Partially mitigated** — RLS is not active until Auth middleware is connected (Etapa 4). |
696
+
";
697
+
// Widths near the one that triggered the original panic.
698
+
for w in[60usize,80,100,120,160]{
699
+
let lines = markdown_to_lines(md, w);
700
+
assert!(!lines.is_empty());
701
+
}
702
+
}
703
+
704
+
#[test]
705
+
fnproportional_distribution_respects_budget(){
706
+
let header:Vec<String> = vec!["A".to_string(),"B".to_string()];
707
+
let body:Vec<Vec<String>> = vec![vec![
708
+
"数据数据数据数据".to_string(),
709
+
"テストテストテスト".to_string(),
710
+
]];
711
+
let available = 30;
712
+
let widths = compute_column_widths(&header,&body, available);
713
+
let border_overhead = 2 + (widths.len() - 1)*3 + 2;
0 commit comments