pub fn floor_char_boundary(s: &str, byte_limit: usize) -> usize {
if byte_limit >= s.len() {
return s.len();
}
let mut i = byte_limit;
while i > 0 && !s.is_char_boundary(i) {
i -= 1;
}
i
}
pub fn truncate_str(s: &str, max_chars: usize) -> String {
truncate_impl(s, max_chars, "...")
}
pub fn truncate_with_note(s: &str, max_chars: usize) -> String {
truncate_impl(s, max_chars, "\n... (truncated)")
}
fn truncate_impl(s: &str, max_chars: usize, suffix: &str) -> String {
if s.len() <= max_chars {
return s.to_string();
}
let char_count = s.chars().count();
if char_count <= max_chars {
return s.to_string();
}
let suffix_len = suffix.chars().count();
if max_chars <= suffix_len {
return suffix.chars().take(max_chars).collect();
}
let truncated: String = s.chars().take(max_chars - suffix_len).collect();
format!("{}{}", truncated, suffix)
}
pub fn truncation_notice(shown_chars: usize, total_chars: usize) -> String {
let omitted = total_chars.saturating_sub(shown_chars);
format!(
"[β OUTPUT TRUNCATED β {shown} of {total} characters shown; {omitted} omitted and \
NOT visible to you. Do NOT enumerate, list, count, or quote any item that is not \
literally present in the text you can see β inventing the omitted content is an \
error. If the user needs the full result, tell them it is longer than you can see \
and re-run with a narrower filter, a count (e.g. `wc -l`), or pagination.]",
shown = shown_chars,
total = total_chars,
omitted = omitted,
)
}
pub fn extract_json_object(raw: &str) -> Option<String> {
let trimmed = raw.trim();
let candidate = if trimmed.starts_with("```") {
trimmed
.trim_start_matches("```json")
.trim_start_matches("```JSON")
.trim_start_matches("```")
.trim_end_matches("```")
.trim()
.to_string()
} else {
trimmed.to_string()
};
if serde_json::from_str::<serde_json::Value>(&candidate)
.ok()
.is_some_and(|v| v.is_object())
{
return Some(candidate);
}
let start = raw.find('{')?;
let end = raw.rfind('}')?;
if end <= start {
return None;
}
let sliced = raw[start..=end].trim().to_string();
if serde_json::from_str::<serde_json::Value>(&sliced)
.ok()
.is_some_and(|v| v.is_object())
{
Some(sliced)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_truncation_needed() {
assert_eq!(truncate_str("hello", 10), "hello");
assert_eq!(truncate_str("hello", 5), "hello");
assert_eq!(truncate_str("", 10), "");
}
#[test]
fn test_truncation_notice_reports_amounts_and_forbids_fabrication() {
let notice = truncation_notice(100, 250);
assert!(notice.contains("OUTPUT TRUNCATED"));
assert!(notice.contains("100 of 250"));
assert!(notice.contains("150 omitted"));
assert!(notice.contains("Do NOT enumerate"));
}
#[test]
fn test_truncation_notice_saturates_when_shown_exceeds_total() {
let notice = truncation_notice(300, 250);
assert!(notice.contains("0 omitted"));
}
#[test]
fn test_truncation_ascii() {
assert_eq!(truncate_str("hello world", 8), "hello...");
assert_eq!(truncate_str("hello world", 7), "hell...");
assert_eq!(truncate_str("abcdefghij", 6), "abc...");
}
#[test]
fn test_truncation_emoji() {
assert_eq!(truncate_str("π¦π¦π¦π¦π¦", 5), "π¦π¦π¦π¦π¦"); assert_eq!(truncate_str("π¦π¦π¦π¦π¦", 4), "π¦..."); assert_eq!(truncate_str("π¦π¦π¦π¦π¦π¦", 5), "π¦π¦..."); assert_eq!(truncate_str("π¦π¦π¦π¦π¦π¦π¦", 6), "π¦π¦π¦..."); }
#[test]
fn test_truncation_mixed() {
assert_eq!(truncate_str("hi π¦ world", 8), "hi π¦ ...");
assert_eq!(truncate_str("β
οΈ wrangler 4.62.0", 10), "β
οΈ wran...");
}
#[test]
fn test_edge_cases() {
assert_eq!(truncate_str("hello", 3), "...");
assert_eq!(truncate_str("hello", 2), "..");
assert_eq!(truncate_str("hello", 1), ".");
assert_eq!(truncate_str("hello", 0), "");
assert_eq!(truncate_str("hello", 5), "hello");
assert_eq!(truncate_str("hello!", 6), "hello!");
}
#[test]
fn test_unicode_various() {
assert_eq!(truncate_str("hΓ©llo wΓΆrld", 8), "hΓ©llo...");
assert_eq!(truncate_str("ζ₯ζ¬θͺγγΉγ", 5), "ζ₯ζ¬...");
assert_eq!(truncate_str("πβ¨β
οΈπ¦", 4), "π...");
}
#[test]
fn test_variation_selectors() {
let s = "β
οΈ test";
let result = truncate_str(s, 5);
assert!(result.len() <= 20); }
#[test]
fn test_truncate_with_note() {
use super::truncate_with_note;
assert_eq!(truncate_with_note("hello", 20), "hello");
let result = truncate_with_note("hello world this is a long string", 30);
assert!(result.ends_with("\n... (truncated)"));
assert!(result.starts_with("hello"));
let result = truncate_with_note("π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦", 20);
assert_eq!(result, "π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦");
let result = truncate_with_note("π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦π¦", 20);
assert!(result.contains("π¦"));
assert!(result.ends_with("\n... (truncated)"));
}
mod proptest_truncate {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn truncate_result_within_limit(s in ".*", n in 0usize..500) {
let result = truncate_str(&s, n);
assert!(result.chars().count() <= n.max(1));
}
#[test]
fn no_truncation_when_fits(s in "[a-z]{0,50}", n in 50usize..200) {
let result = truncate_str(&s, n);
if s.chars().count() <= n {
assert_eq!(result, s);
}
}
#[test]
fn truncate_never_panics(s in "\\PC{0,500}", n in 0usize..1000) {
let _ = truncate_str(&s, n);
let _ = truncate_with_note(&s, n);
}
}
}
}