huskies: merge 927

This commit is contained in:
dave
2026-05-12 17:49:44 +00:00
parent b8945654bf
commit 03a99b3cf1
33 changed files with 119 additions and 25 deletions
+62
View File
@@ -3,6 +3,23 @@
//! These functions are transport-agnostic helpers for processing chat messages:
//! prefix stripping, bot-mention handling, and paragraph buffering.
/// Truncate `s` to at most `max_bytes` bytes without splitting a UTF-8 codepoint.
///
/// If `s.len() <= max_bytes` the original slice is returned unchanged.
/// Otherwise the returned slice ends at the largest char boundary ≤ `max_bytes`,
/// preventing any panic that would result from slicing mid-codepoint.
pub fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
if s.len() <= max_bytes {
return s;
}
let mut boundary = max_bytes;
while !s.is_char_boundary(boundary) {
boundary -= 1;
}
#[allow(clippy::string_slice)] // boundary is guaranteed to be a char boundary by the loop above
&s[..boundary]
}
/// Returns `true` if the message body is an affirmative permission response.
///
/// Recognised affirmative tokens (case-insensitive): `yes`, `y`, `approve`,
@@ -26,6 +43,7 @@ pub fn is_permission_approval(body: &str) -> bool {
/// Case-insensitive prefix strip that also requires the match to end at a
/// word boundary (whitespace, punctuation, or end-of-string).
#[allow(clippy::string_slice)] // prefix.len() is safe: `get(..prefix.len())` already validated the boundary
pub fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
let candidate = text.get(..prefix.len())?;
if !candidate.eq_ignore_ascii_case(prefix) {
@@ -50,6 +68,7 @@ pub fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
/// - `DisplayName, rest` → `rest` (Element tab-completion may insert a comma)
/// - `DisplayName ⚡️: rest` → `rest` (display name with emoji)
/// - `[DisplayName](https://matrix.to/#/@user:server) rest` → `rest` (Element mention pill)
#[allow(clippy::string_slice)] // all indices come from str::find / str::find_map → always char boundaries
pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
let trimmed = message.trim();
@@ -98,6 +117,7 @@ pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str
/// emoji from display names (e.g. `Timmy ⚡️`), and Element tab-completion
/// separators (`:` or `,`). This function skips all of that and returns a
/// slice starting at the first ASCII alphanumeric character (the command).
#[allow(clippy::string_slice)] // byte_skip comes from char_indices → guaranteed char boundary
fn strip_mention_separator(rest: &str) -> &str {
let byte_skip = rest
.char_indices()
@@ -132,6 +152,7 @@ fn is_inside_code_fence(text: &str) -> bool {
/// block (delimited by ` ``` ` lines) is **not** treated as a paragraph
/// boundary. This prevents a blank line inside a code block from splitting
/// the fence across multiple messages, which would corrupt the rendering.
#[allow(clippy::string_slice)] // abs_pos comes from str::find → always a char boundary
pub fn drain_complete_paragraphs(buffer: &mut String) -> Vec<String> {
let mut paragraphs = Vec::new();
let mut search_from = 0;
@@ -259,6 +280,47 @@ pub fn normalize_line_breaks(text: &str) -> String {
mod tests {
use super::*;
// -- truncate_at_char_boundary ------------------------------------------
#[test]
fn truncate_ascii_within_limit() {
assert_eq!(truncate_at_char_boundary("hello", 10), "hello");
}
#[test]
fn truncate_ascii_at_exact_limit() {
assert_eq!(truncate_at_char_boundary("hello", 5), "hello");
}
#[test]
fn truncate_ascii_over_limit() {
assert_eq!(truncate_at_char_boundary("hello world", 5), "hello");
}
#[test]
fn truncate_multibyte_mid_codepoint_snaps_back() {
// "héllo": 'é' is U+00E9, 2 bytes. max_bytes=2 lands inside it → snap to 1.
let s = "héllo";
assert_eq!(truncate_at_char_boundary(s, 2), "h");
}
#[test]
fn truncate_multibyte_on_char_boundary() {
// "héllo": h(1) + é(2) = 3 bytes. max_bytes=3 is a valid boundary.
let s = "héllo";
assert_eq!(truncate_at_char_boundary(s, 3), "");
}
#[test]
fn truncate_max_bytes_zero_returns_empty() {
assert_eq!(truncate_at_char_boundary("hello", 0), "");
}
#[test]
fn truncate_max_bytes_greater_than_len() {
assert_eq!(truncate_at_char_boundary("hi", 100), "hi");
}
// -- is_permission_approval ---------------------------------------------
#[test]