/// Maximum input length for wrap/format operations (10 MB). /// Callers should check against this constant before invoking `wrap_line_chars` /// to avoid pathological cases on oversized lines. pub const MAX_WRAP_INPUT_LEN: usize = 10 * 1024 * 1024; /// Column spacing for tab stop alignment. const TAB_WIDTH: usize = 4; /// Split a line into chunks of exactly `width` display columns. /// For a log viewer, we want character-level wrapping, not word-level. /// Uses `unicode-width` for correct CJK/emoji/zero-width handling. /// Tab characters expand to the next tab-stop boundary and split across /// rows when the expansion exceeds the remaining width. pub fn wrap_line_chars(line: &str, width: usize) -> Vec { use unicode_width::UnicodeWidthChar; if width == 0 { return vec![String::new()]; } if line.is_empty() { return vec![String::new()]; } let mut result = Vec::new(); let mut row = String::new(); let mut col = 0; for ch in line.chars() { if ch == '\t' { let tab_stop = TAB_WIDTH - (col % TAB_WIDTH); let mut remaining = tab_stop; while remaining > 0 { let avail = width.saturating_sub(col); let fill = remaining.min(avail); for _ in 0..fill { row.push(' '); } col += fill; remaining -= fill; if col >= width { result.push(std::mem::take(&mut row)); col = 0; } } } else { let w = if ch.is_control() { // Control characters (except tab): width 0, still pushed to preserve content. // Visible rendering is the caller's responsibility. 0 } else { ch.width().unwrap_or(0) }; if col + w > width && !row.is_empty() { result.push(std::mem::take(&mut row)); col = 0; } row.push(ch); col += w; if col >= width { result.push(std::mem::take(&mut row)); col = 0; } } } if !row.is_empty() { result.push(row); } if result.is_empty() { result.push(String::new()); } result } /// Format a line as pretty-printed JSON if it's a JSON Object. /// Returns the original line unchanged for non-JSON or non-Object content. pub fn format_json_line(line: &str) -> String { if line.trim().is_empty() { return String::new(); } // Quick pre-check: only try parsing if it starts with '{' if !line.trim_start().starts_with('{') { return line.to_string(); } match serde_json::from_str::(line) { Ok(value) if value.is_object() => { serde_json::to_string_pretty(&value).unwrap_or_else(|_| line.to_string()) } _ => line.to_string(), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_wrap_empty_line() { let result = wrap_line_chars("", 80); assert_eq!(result, vec![""]); } #[test] fn test_wrap_zero_width() { let result = wrap_line_chars("hello", 0); assert_eq!(result, vec![""]); } #[test] fn test_wrap_short_line() { let result = wrap_line_chars("hello", 80); assert_eq!(result, vec!["hello"]); } #[test] fn test_wrap_exact_width() { let result = wrap_line_chars("abc", 3); assert_eq!(result, vec!["abc"]); } #[test] fn test_wrap_multi_row() { let result = wrap_line_chars("abcdef", 3); assert_eq!(result, vec!["abc", "def"]); } #[test] fn test_wrap_with_tab() { let result = wrap_line_chars("a\tb", 4); assert_eq!(result, vec!["a ", "b"]); } #[test] fn test_format_json_empty() { assert_eq!(format_json_line(""), ""); assert_eq!(format_json_line(" "), ""); } #[test] fn test_format_json_non_json() { assert_eq!(format_json_line("hello world"), "hello world"); } #[test] fn test_format_json_valid_object() { let input = r#"{"key":"value"}"#; let output = format_json_line(input); assert!( output.contains('\n'), "pretty-printed JSON should have newlines" ); assert!(output.contains("key")); assert!(output.contains("value")); } #[test] fn test_format_json_array_unchanged() { let input = r#"[1,2,3]"#; assert_eq!(format_json_line(input), input); } #[test] fn test_max_wrap_input_len_constant() { assert_eq!(MAX_WRAP_INPUT_LEN, 10 * 1024 * 1024); } #[test] fn test_wrap_cjk_chars() { let result = wrap_line_chars("你好", 3); assert_eq!(result, vec!["你", "好"]); } #[test] fn test_wrap_cjk_ascii_mixed() { let result = wrap_line_chars("a你好", 4); assert_eq!(result, vec!["a你", "好"]); } #[test] fn test_wrap_zero_width_char() { let result = wrap_line_chars("a\u{200B}b", 2); assert_eq!(result, vec!["a\u{200B}b"]); } #[test] fn test_wrap_emoji() { let result = wrap_line_chars("😀a", 3); assert_eq!(result, vec!["😀a"]); } #[test] fn test_wrap_emoji_exact_wrap() { let result = wrap_line_chars("😀a", 2); assert_eq!(result, vec!["😀", "a"]); } #[test] fn test_wrap_combining_mark() { // Scalar-width wrapping: combining mark (width 0) stays with next base char, // not the preceding one, because the base char already triggered a flush. let result = wrap_line_chars("a\u{0301}b", 1); assert_eq!(result, vec!["a", "\u{0301}b"]); } #[test] fn test_wrap_cjk_width_one() { let result = wrap_line_chars("你好", 1); assert_eq!(result, vec!["你", "好"]); } #[test] fn test_tab_narrow_width() { let result = wrap_line_chars("\t", 2); assert_eq!(result, vec![" ", " "]); let result = wrap_line_chars("\t", 1); assert_eq!(result, vec![" ", " ", " ", " "]); } #[test] fn test_tab_stop_alignment() { assert_eq!(wrap_line_chars("a\tb", 8), vec!["a b"]); assert_eq!(wrap_line_chars("ab\t", 4), vec!["ab "]); assert_eq!(wrap_line_chars("abc\tb", 8), vec!["abc b"]); } #[test] fn test_tab_at_line_boundary() { let result = wrap_line_chars("a\tb", 4); assert_eq!(result, vec!["a ", "b"]); } #[test] fn test_tab_regression_ab_tab() { let result = wrap_line_chars("ab\t", 4); assert_eq!(result, vec!["ab "]); } }