use std::time::Instant;
pub struct StreamingMessageBuffer {
accumulated: String,
last_flush: Instant,
last_flushed_content: String,
flush_interval_ms: u64,
}
impl StreamingMessageBuffer {
pub fn new(flush_interval_ms: u64) -> Self {
Self {
accumulated: String::new(),
last_flush: Instant::now(),
last_flushed_content: String::new(),
flush_interval_ms,
}
}
pub fn default_interval() -> Self {
Self::new(500)
}
pub fn push(&mut self, token: &str) {
self.accumulated.push_str(token);
}
pub fn should_flush(&self) -> bool {
self.last_flush.elapsed().as_millis() as u64 >= self.flush_interval_ms
}
pub fn has_new_content(&self) -> bool {
self.accumulated != self.last_flushed_content
}
pub fn flush(&mut self) -> Option<String> {
if !self.has_new_content() {
return None;
}
self.last_flush = Instant::now();
let healed = heal_markdown(&self.accumulated);
self.last_flushed_content = self.accumulated.clone();
Some(healed)
}
pub fn finalize(&mut self) -> String {
std::mem::take(&mut self.accumulated)
}
pub fn len(&self) -> usize {
self.accumulated.len()
}
pub fn is_empty(&self) -> bool {
self.accumulated.is_empty()
}
}
fn heal_markdown(input: &str) -> String {
let mut result = input.to_string();
let fence_count = result.matches("```").count();
if fence_count % 2 != 0 {
result.push_str("\n```");
}
if fence_count % 2 == 0 {
let backtick_count = count_unescaped_markers(&result, '`');
if backtick_count % 2 != 0 {
result.push('`');
}
let bold_count = count_double_markers(&result, '*');
if bold_count % 2 != 0 {
result.push_str("**");
}
let strike_count = count_double_markers(&result, '~');
if strike_count % 2 != 0 {
result.push_str("~~");
}
let italic_remaining = count_single_after_doubles(&result, '*');
if italic_remaining % 2 != 0 {
result.push('*');
}
}
result
}
fn count_unescaped_markers(text: &str, marker: char) -> usize {
let no_fences = text.replace("```", "");
no_fences.chars().filter(|&c| c == marker).count()
}
fn count_double_markers(text: &str, ch: char) -> usize {
let marker: String = std::iter::repeat(ch).take(2).collect();
text.matches(&marker).count()
}
fn count_single_after_doubles(text: &str, ch: char) -> usize {
let double: String = std::iter::repeat(ch).take(2).collect();
let without_doubles = text.replace(&double, "");
without_doubles.chars().filter(|&c| c == ch).count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_heal_unclosed_bold() {
assert_eq!(heal_markdown("hello **world"), "hello **world**");
}
#[test]
fn test_heal_closed_bold_unchanged() {
assert_eq!(heal_markdown("hello **world**"), "hello **world**");
}
#[test]
fn test_heal_unclosed_code() {
assert_eq!(heal_markdown("run `npm install"), "run `npm install`");
}
#[test]
fn test_heal_unclosed_code_block() {
assert_eq!(
heal_markdown("```python\nprint('hello')"),
"```python\nprint('hello')\n```"
);
}
#[test]
fn test_heal_closed_code_block_unchanged() {
let input = "```python\nprint('hello')\n```";
assert_eq!(heal_markdown(input), input);
}
#[test]
fn test_heal_unclosed_strikethrough() {
assert_eq!(heal_markdown("~~deleted"), "~~deleted~~");
}
#[test]
fn test_heal_complete_markdown_unchanged() {
let input = "**bold** and *italic* and `code` and ~~strike~~";
assert_eq!(heal_markdown(input), input);
}
#[test]
fn test_empty_string() {
assert_eq!(heal_markdown(""), "");
}
#[test]
fn test_buffer_push_and_flush() {
let mut buf = StreamingMessageBuffer::new(0); buf.push("hello ");
buf.push("**world");
assert!(buf.should_flush());
let content = buf.flush().unwrap();
assert_eq!(content, "hello **world**");
let final_content = buf.finalize();
assert_eq!(final_content, "hello **world"); }
#[test]
fn test_buffer_no_flush_when_no_new_content() {
let mut buf = StreamingMessageBuffer::new(0);
buf.push("hello");
buf.flush(); assert!(buf.flush().is_none()); }
}