Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
607 lines
19 KiB
Rust
607 lines
19 KiB
Rust
//! Line and string utility functions for ratatui text manipulation.
|
||
|
||
use ratatui::text::{Line, Span};
|
||
use unicode_segmentation::UnicodeSegmentation;
|
||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||
|
||
pub use super::tool_paths::{path_basename, path_for_tool_header, shorten_path};
|
||
|
||
/// Clone a borrowed ratatui `Line` into an owned `'static` line.
|
||
pub fn line_to_static(line: &Line<'_>) -> Line<'static> {
|
||
Line {
|
||
style: line.style,
|
||
alignment: line.alignment,
|
||
spans: line
|
||
.spans
|
||
.iter()
|
||
.map(|s| Span {
|
||
style: s.style,
|
||
content: std::borrow::Cow::Owned(s.content.to_string()),
|
||
})
|
||
.collect(),
|
||
}
|
||
}
|
||
|
||
/// Append owned copies of borrowed lines to `out`.
|
||
pub fn push_owned_lines(src: &[Line<'_>], out: &mut Vec<Line<'static>>) {
|
||
for l in src {
|
||
out.push(line_to_static(l));
|
||
}
|
||
}
|
||
|
||
/// True for a character unsafe to render from untrusted/server text:
|
||
/// C0/C1 controls (the terminal-escape-injection vector) plus the Unicode
|
||
/// bidi-control and zero-width/format set (Trojan-Source spoofing) — U+061C,
|
||
/// U+200B–200F, U+202A–202E, U+2060–206F, U+FEFF.
|
||
///
|
||
/// Shared by every untrusted-text strip/scrub site (chip labels, toast error
|
||
/// scrub, settings editor input) so the set never drifts between them.
|
||
pub fn is_unsafe_display_char(c: char) -> bool {
|
||
c.is_control()
|
||
|| matches!(
|
||
c,
|
||
'\u{061C}'
|
||
| '\u{200B}'..='\u{200F}'
|
||
| '\u{202A}'..='\u{202E}'
|
||
| '\u{2060}'..='\u{206F}'
|
||
| '\u{FEFF}'
|
||
)
|
||
}
|
||
|
||
/// Polyfill for nightly-only [`str::floor_char_boundary`].
|
||
/// Snaps a byte index down to the nearest char boundary.
|
||
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
|
||
let index = index.min(s.len());
|
||
let mut i = index;
|
||
while i > 0 && !s.is_char_boundary(i) {
|
||
i -= 1;
|
||
}
|
||
i
|
||
}
|
||
|
||
/// Byte offset at which cumulative display width exceeds `max_width`.
|
||
/// Returns `s.len()` when the entire string fits.
|
||
pub fn byte_offset_at_width(s: &str, max_width: usize) -> usize {
|
||
let mut width = 0;
|
||
for (i, ch) in s.char_indices() {
|
||
let cw = UnicodeWidthChar::width(ch).unwrap_or(0);
|
||
if width + cw > max_width {
|
||
return i;
|
||
}
|
||
width += cw;
|
||
}
|
||
s.len()
|
||
}
|
||
|
||
/// Truncate a string to fit within `max_width` display columns.
|
||
///
|
||
/// Uses Unicode-aware width measurement (handles CJK wide chars,
|
||
/// multi-byte UTF-8 like em-dash, etc.). If truncated, the last character
|
||
/// is replaced with `…` so the result fits within `max_width`.
|
||
///
|
||
/// Returns the original string (owned) if it already fits.
|
||
pub fn truncate_str(s: &str, max_width: usize) -> String {
|
||
if max_width == 0 {
|
||
return String::new();
|
||
}
|
||
|
||
let end = byte_offset_at_width(s, max_width);
|
||
let needs_ellipsis = end < s.len();
|
||
|
||
if needs_ellipsis && max_width > 1 {
|
||
// Back up one char to make room for '…' (1 display column).
|
||
let truncated_end = s[..end]
|
||
.char_indices()
|
||
.next_back()
|
||
.map(|(i, _)| i)
|
||
.unwrap_or(0);
|
||
format!("{}…", &s[..truncated_end])
|
||
} else if needs_ellipsis {
|
||
"…".to_string()
|
||
} else {
|
||
s[..end].to_string()
|
||
}
|
||
}
|
||
|
||
/// Truncate a styled `Line` (multiple spans) to fit within `max_width` display columns.
|
||
///
|
||
/// Walks spans left-to-right, consuming width budget. When the budget is
|
||
/// exhausted mid-span, that span is truncated and `…` is appended. Spans
|
||
/// beyond the budget are dropped. All styles are preserved.
|
||
///
|
||
/// Returns the line `unchanged` if it already fits.
|
||
pub fn truncate_line(line: Line<'static>, max_width: usize) -> Line<'static> {
|
||
if max_width == 0 {
|
||
return Line::from(vec![]);
|
||
}
|
||
|
||
let total: usize = line.spans.iter().map(|s| s.content.width()).sum();
|
||
if total <= max_width {
|
||
return line;
|
||
}
|
||
|
||
// Need room for the ellipsis (1 column).
|
||
let budget = max_width.saturating_sub(1);
|
||
let mut used = 0usize;
|
||
let mut out: Vec<Span<'static>> = Vec::new();
|
||
|
||
for span in line.spans {
|
||
let sw = span.content.width();
|
||
if used + sw <= budget {
|
||
// Entire span fits.
|
||
used += sw;
|
||
out.push(span);
|
||
} else {
|
||
// Partial fit — truncate this span.
|
||
let remaining = budget - used;
|
||
if remaining > 0 {
|
||
let truncated = take_width(&span.content, remaining);
|
||
out.push(Span::styled(truncated, span.style));
|
||
}
|
||
// Append ellipsis with the same style as the last span.
|
||
let ellipsis_style = out.last().map(|s| s.style).unwrap_or_default();
|
||
out.push(Span::styled("\u{2026}", ellipsis_style));
|
||
return Line::from(out);
|
||
}
|
||
}
|
||
|
||
// Shouldn't reach here (total > max_width checked above), but be safe.
|
||
Line::from(out)
|
||
}
|
||
|
||
/// Clip or pad a styled `Line` to exactly `width` display columns.
|
||
///
|
||
/// Wider lines are clipped on grapheme boundaries (a multi-`char` grapheme like
|
||
/// `⚠\u{FE0F}` is never split) with no ellipsis; narrower lines are `padded` with
|
||
/// trailing spaces. This keeps a rendered row "self-owning" — the app writes a
|
||
/// real cell in every column, so a terminal drawing a glyph wider than the app
|
||
/// measured cannot strand a stale cell past the row (the markdown-table ghost
|
||
/// glyph bug). Width uses [`UnicodeWidthStr`], matching the table layout.
|
||
///
|
||
/// `width` must be a bounded display width: the pad branch allocates
|
||
/// `width - total` spaces.
|
||
pub fn fit_line_to_width<'a>(line: Line<'a>, width: usize) -> Line<'a> {
|
||
let total: usize = line.spans.iter().map(|s| s.content.width()).sum();
|
||
if total == width {
|
||
return line;
|
||
}
|
||
|
||
let Line {
|
||
style,
|
||
alignment,
|
||
mut spans,
|
||
} = line;
|
||
|
||
if total < width {
|
||
spans.push(Span::raw(" ".repeat(width - total)));
|
||
return Line {
|
||
style,
|
||
alignment,
|
||
spans,
|
||
};
|
||
}
|
||
|
||
// Wider than width: clip on grapheme boundaries, no ellipsis.
|
||
let mut out: Vec<Span<'a>> = Vec::new();
|
||
let mut used = 0usize;
|
||
for span in spans {
|
||
let sw = span.content.width();
|
||
if used + sw <= width {
|
||
used += sw;
|
||
out.push(span);
|
||
if used == width {
|
||
break;
|
||
}
|
||
continue;
|
||
}
|
||
// This span straddles the boundary — take whole graphemes that fit.
|
||
let remaining = width - used;
|
||
let mut taken = String::new();
|
||
let mut taken_width = 0usize;
|
||
for g in span.content.graphemes(true) {
|
||
let gw = g.width();
|
||
if taken_width + gw > remaining {
|
||
break;
|
||
}
|
||
taken_width += gw;
|
||
taken.push_str(g);
|
||
}
|
||
if !taken.is_empty() {
|
||
out.push(Span::styled(taken, span.style));
|
||
used += taken_width;
|
||
}
|
||
// A straddling wide grapheme leaves a 1-column gap; pad it.
|
||
if used < width {
|
||
out.push(Span::raw(" ".repeat(width - used)));
|
||
}
|
||
break;
|
||
}
|
||
|
||
Line {
|
||
style,
|
||
alignment,
|
||
spans: out,
|
||
}
|
||
}
|
||
|
||
/// Take the first `n` display columns from a string.
|
||
fn take_width(s: &str, n: usize) -> String {
|
||
let mut width = 0;
|
||
let mut end = s.len();
|
||
for (i, ch) in s.char_indices() {
|
||
let cw = ch.width().unwrap_or(0);
|
||
if width + cw > n {
|
||
end = i;
|
||
break;
|
||
}
|
||
width += cw;
|
||
}
|
||
s[..end].to_string()
|
||
}
|
||
|
||
/// Cascade-truncate multiple text elements to fit within `avail` display columns.
|
||
///
|
||
/// Returns `(type, description, activity, meta)` truncated to fit.
|
||
/// Priority (highest first): type, activity, meta. Description is truncated
|
||
/// first. If overhead (type + activity + meta) >= avail, description is dropped
|
||
/// and the remaining elements are cascaded: meta is dropped first, then
|
||
/// activity is truncated, then type.
|
||
pub fn cascade_truncate(
|
||
avail: usize,
|
||
type_text: &str,
|
||
description: &str,
|
||
activity_text: &str,
|
||
meta_text: &str,
|
||
) -> (String, String, String, String) {
|
||
let overhead = type_text.width() + activity_text.width() + meta_text.width();
|
||
if overhead <= avail {
|
||
let desc_max = avail - overhead;
|
||
(
|
||
type_text.to_string(),
|
||
truncate_str(description, desc_max),
|
||
activity_text.to_string(),
|
||
meta_text.to_string(),
|
||
)
|
||
} else {
|
||
let mut budget = avail;
|
||
let td = if type_text.width() <= budget {
|
||
budget -= type_text.width();
|
||
type_text.to_string()
|
||
} else {
|
||
let s = truncate_str(type_text, budget);
|
||
budget = 0;
|
||
s
|
||
};
|
||
let ad = if budget == 0 {
|
||
String::new()
|
||
} else if activity_text.width() <= budget {
|
||
budget -= activity_text.width();
|
||
activity_text.to_string()
|
||
} else {
|
||
let s = truncate_str(activity_text, budget);
|
||
budget = 0;
|
||
s
|
||
};
|
||
let md = if budget == 0 {
|
||
String::new()
|
||
} else if meta_text.width() <= budget {
|
||
meta_text.to_string()
|
||
} else {
|
||
truncate_str(meta_text, budget)
|
||
};
|
||
(td, String::new(), ad, md)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn is_unsafe_display_char_covers_controls_and_bidi_format() {
|
||
// Safe: ordinary printable text (incl. legitimate RTL letters).
|
||
for c in ['a', ' ', '/', '\u{00e9}', '\u{05d0}'] {
|
||
assert!(!is_unsafe_display_char(c), "{c:?} must be safe");
|
||
}
|
||
// Unsafe: C0/C1 controls + the full bidi-control / zero-width set.
|
||
for c in [
|
||
'\u{1b}', '\n', '\t', '\u{061C}', '\u{200B}', '\u{200F}', '\u{202E}', '\u{2066}',
|
||
'\u{2069}', '\u{206F}', '\u{FEFF}',
|
||
] {
|
||
assert!(
|
||
is_unsafe_display_char(c),
|
||
"{:#06x} must be unsafe",
|
||
c as u32
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_str_fits() {
|
||
assert_eq!(truncate_str("hello", 10), "hello");
|
||
assert_eq!(truncate_str("hello", 5), "hello");
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_str_truncates() {
|
||
assert_eq!(truncate_str("hello world!", 5), "hell…");
|
||
assert_eq!(truncate_str("abcdef", 4), "abc…");
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_str_empty_and_zero() {
|
||
assert_eq!(truncate_str("hello", 0), "");
|
||
assert_eq!(truncate_str("", 5), "");
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_str_width_1() {
|
||
assert_eq!(truncate_str("hello", 1), "…");
|
||
assert_eq!(truncate_str("x", 1), "x");
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_str_multibyte() {
|
||
// em-dash is 1 display column but 3 bytes
|
||
let s = "hello — world";
|
||
let result = truncate_str(s, 8);
|
||
assert!(result.ends_with('…'));
|
||
// safe byte length
|
||
assert!(result.len() <= 12);
|
||
}
|
||
|
||
// truncate_line tests
|
||
|
||
#[test]
|
||
fn truncate_line_fits() {
|
||
let line = Line::from(vec![Span::raw("Hello "), Span::raw("world")]);
|
||
let result = truncate_line(line, 20);
|
||
assert_eq!(result.spans.len(), 2);
|
||
assert_eq!(result.spans[0].content.as_ref(), "Hello ");
|
||
assert_eq!(result.spans[1].content.as_ref(), "world");
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_line_cuts_mid_span() {
|
||
let line = Line::from(vec![
|
||
Span::raw("Edit "),
|
||
Span::raw("very/long/path/to/file.rs"),
|
||
]);
|
||
// Total = 29, budget = 15 → "Edit very/long…"
|
||
let result = truncate_line(line, 15);
|
||
let text: String = result.spans.iter().map(|s| s.content.as_ref()).collect();
|
||
assert!(text.ends_with('\u{2026}'));
|
||
assert!(text.width() <= 15);
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_line_drops_later_spans() {
|
||
let line = Line::from(vec![
|
||
Span::raw("Search "),
|
||
Span::raw("pattern"),
|
||
Span::raw(" in "),
|
||
Span::raw("path"),
|
||
Span::raw(" (5 matches)"),
|
||
]);
|
||
let result = truncate_line(line, 18);
|
||
let text: String = result.spans.iter().map(|s| s.content.as_ref()).collect();
|
||
assert!(text.ends_with('\u{2026}'));
|
||
assert!(text.width() <= 18);
|
||
}
|
||
|
||
#[test]
|
||
fn truncate_line_zero_width() {
|
||
let line = Line::from(vec![Span::raw("hello")]);
|
||
let result = truncate_line(line, 0);
|
||
assert!(result.spans.is_empty());
|
||
}
|
||
|
||
// fit_line_to_width tests
|
||
|
||
fn line_text(line: &Line<'static>) -> String {
|
||
line.spans.iter().map(|s| s.content.as_ref()).collect()
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_pads_short_line() {
|
||
let line = Line::from(vec![Span::raw("│ a │")]);
|
||
let out = fit_line_to_width(line, 10);
|
||
assert_eq!(line_text(&out).width(), 10);
|
||
assert_eq!(line_text(&out), "│ a │ ");
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_exact_width_unchanged() {
|
||
let line = Line::from(vec![Span::raw("hello")]);
|
||
let out = fit_line_to_width(line, 5);
|
||
assert_eq!(out.spans.len(), 1);
|
||
assert_eq!(line_text(&out), "hello");
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_clips_long_line_no_ellipsis() {
|
||
let line = Line::from(vec![Span::raw("│ Column A │ Column B │")]);
|
||
let out = fit_line_to_width(line, 8);
|
||
assert_eq!(line_text(&out).width(), 8);
|
||
assert_eq!(line_text(&out), "│ Column");
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_does_not_split_emoji_grapheme() {
|
||
// a(1)+b(1)+⚠️(2) = 4. Clipping to 3 must drop the width-2 grapheme
|
||
// whole (never split it) and pad → "ab" + 1 space.
|
||
let line = Line::from(vec![Span::raw("ab\u{26A0}\u{FE0F}")]);
|
||
let out = fit_line_to_width(line, 3);
|
||
assert_eq!(line_text(&out).width(), 3);
|
||
assert_eq!(line_text(&out), "ab ");
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_clips_grapheme_straddle_in_later_span() {
|
||
// The straddle happens in a later span: keep "ab", then 1 col left →
|
||
// ⚠️ (width 2) won't fit → dropped whole and `padded`.
|
||
let line = Line::from(vec![Span::raw("ab"), Span::raw("\u{26A0}\u{FE0F}cd")]);
|
||
let out = fit_line_to_width(line, 3);
|
||
assert_eq!(line_text(&out).width(), 3);
|
||
assert_eq!(line_text(&out), "ab ");
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_drops_subsequent_spans_after_clip() {
|
||
let line = Line::from(vec![
|
||
Span::raw("hello"),
|
||
Span::raw(" world"),
|
||
Span::raw("!!!"),
|
||
]);
|
||
let out = fit_line_to_width(line, 5);
|
||
assert_eq!(line_text(&out), "hello");
|
||
// The straddling/later spans must be dropped entirely.
|
||
assert_eq!(out.spans.len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_takes_partial_of_later_span() {
|
||
let line = Line::from(vec![Span::raw("ab"), Span::raw("cdef")]);
|
||
let out = fit_line_to_width(line, 4);
|
||
assert_eq!(line_text(&out), "abcd");
|
||
assert_eq!(line_text(&out).width(), 4);
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_zero_width_returns_empty() {
|
||
let line = Line::from(vec![Span::raw("│ a │")]);
|
||
let out = fit_line_to_width(line, 0);
|
||
assert_eq!(line_text(&out), "");
|
||
assert_eq!(line_text(&out).width(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn fit_line_preserves_span_styles_when_padding() {
|
||
let bold = ratatui::style::Style::new().add_modifier(ratatui::style::Modifier::BOLD);
|
||
let line = Line::from(vec![Span::styled("hi", bold)]);
|
||
let out = fit_line_to_width(line, 5);
|
||
assert_eq!(line_text(&out).width(), 5);
|
||
assert!(
|
||
out.spans[0]
|
||
.style
|
||
.add_modifier
|
||
.contains(ratatui::style::Modifier::BOLD)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn legacy_tool_path_api_remains_available_at_line_utils_path() {
|
||
assert_eq!(shorten_path("verylongfilename.rs", 10), "verylongf…");
|
||
assert_eq!(path_basename("/repo/src/main.rs", 80), "main.rs");
|
||
assert_eq!(
|
||
path_for_tool_header("/repo/src/main.rs", Some(80), "Read ".len()),
|
||
"main.rs"
|
||
);
|
||
assert_eq!(
|
||
path_for_tool_header("/repo/src/main.rs", None, "Read ".len()),
|
||
"/repo/src/main.rs"
|
||
);
|
||
}
|
||
|
||
// cascade_truncate tests
|
||
|
||
#[test]
|
||
fn cascade_truncate_all_fit() {
|
||
let (t, d, a, m) =
|
||
cascade_truncate(50, "type ", "description", " \u{2014} running", " meta");
|
||
assert_eq!(t, "type ");
|
||
assert_eq!(d, "description");
|
||
assert_eq!(a, " \u{2014} running");
|
||
assert_eq!(m, " meta");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_desc_truncated() {
|
||
let (t, d, a, m) = cascade_truncate(
|
||
25,
|
||
"type ",
|
||
"long description here",
|
||
" \u{2014} running",
|
||
" meta",
|
||
);
|
||
assert_eq!(t, "type ");
|
||
assert_eq!(d, "lo\u{2026}");
|
||
assert_eq!(a, " \u{2014} running");
|
||
assert_eq!(m, " meta");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_desc_gone_meta_truncated() {
|
||
// overhead = 6+10+6 = 22 > avail 20 → desc gone, type 6 + activity 10 + meta truncated to 4
|
||
let (t, d, a, m) = cascade_truncate(20, "type ", "desc", " \u{2014} running", " meta");
|
||
assert_eq!(t, "type ");
|
||
assert_eq!(d, "");
|
||
assert_eq!(a, " \u{2014} running");
|
||
assert_eq!(m, " m\u{2026}");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_meta_and_activity_gone() {
|
||
// avail=8, type=6 fits (budget=2), activity truncated to 2, meta gone
|
||
let (t, d, a, m) = cascade_truncate(8, "type ", "desc", " \u{2014} running", " meta");
|
||
assert_eq!(t, "type ");
|
||
assert_eq!(d, "");
|
||
assert_eq!(a, " \u{2026}");
|
||
assert_eq!(m, "");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_type_truncated() {
|
||
let (t, d, a, m) = cascade_truncate(3, "type ", "desc", " \u{2014} running", " meta");
|
||
assert_eq!(t, "ty\u{2026}");
|
||
assert_eq!(d, "");
|
||
assert_eq!(a, "");
|
||
assert_eq!(m, "");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_zero_avail() {
|
||
let (t, d, a, m) = cascade_truncate(0, "type ", "desc", " \u{2014} running", " meta");
|
||
assert_eq!(t, "");
|
||
assert_eq!(d, "");
|
||
assert_eq!(a, "");
|
||
assert_eq!(m, "");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_unicode() {
|
||
// ✗ = 1 display column; — = 1 display column
|
||
let (t, d, a, m) = cascade_truncate(10, "\u{2717} ", "description", " \u{2014} run", "");
|
||
assert_eq!(t, "\u{2717} ");
|
||
assert_eq!(d, "\u{2026}");
|
||
assert_eq!(a, " \u{2014} run");
|
||
assert_eq!(m, "");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_overhead_equals_avail() {
|
||
// overhead exactly equals avail → desc empty, everything else fits
|
||
let (t, d, a, m) = cascade_truncate(22, "type ", "desc", " \u{2014} running", " meta");
|
||
assert_eq!(t, "type ");
|
||
assert_eq!(d, "");
|
||
assert_eq!(a, " \u{2014} running");
|
||
assert_eq!(m, " meta");
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_avail_one() {
|
||
let (t, d, a, m) = cascade_truncate(1, "type", "desc", "act", "meta");
|
||
assert_eq!(t, "\u{2026}");
|
||
assert_eq!((d.as_str(), a.as_str(), m.as_str()), ("", "", ""));
|
||
}
|
||
|
||
#[test]
|
||
fn cascade_truncate_all_empty() {
|
||
let (t, d, a, m) = cascade_truncate(10, "", "", "", "");
|
||
assert_eq!(
|
||
(t.as_str(), d.as_str(), a.as_str(), m.as_str()),
|
||
("", "", "", "")
|
||
);
|
||
}
|
||
}
|