use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
use pulldown_cmark_to_cmark::{cmark_resume, State};
use sapiens::chains::Message;
use sapiens::context::{ChatEntry, ChatEntryFormatter, MessageFormatter};
use sapiens::models::Role;
pub struct Formatter {}
impl ChatEntryFormatter for Formatter {
fn format(&self, entry: &ChatEntry) -> String {
let msg = entry.msg.clone();
match entry.role {
Role::User => format!(":earth_americas:\n{}", msg),
Role::Assistant => format!(":robot:\n{}", msg),
Role::System => format!(":rooster:\n{}", msg),
Role::Function => format!(":gear:\n{}", msg),
Role::Tool => format!(":wrench:\n{}", msg),
}
}
}
impl MessageFormatter for Formatter {
fn format(&self, msg: &Message) -> String {
msg.to_string()
}
}
fn md_event_size(event: &Event) -> usize {
match event {
Event::Text(text) => text.len(),
Event::Code(text) => text.len(),
Event::Html(text) => text.len(),
Event::FootnoteReference(text) => text.len(),
Event::SoftBreak => 1,
Event::HardBreak => 2,
Event::Rule => 1,
Event::TaskListMarker(_) => 3,
Event::Start(tag) => {
let len = match tag {
Tag::Paragraph => 2,
Tag::Heading(level, id, classes) => {
*level as usize
+ 1
+ id.map(|x| x.len()).unwrap_or_default()
+ classes.iter().map(|x| x.len()).sum::<usize>()
}
Tag::BlockQuote => 2,
Tag::CodeBlock(CodeBlockKind::Indented) => 2,
Tag::CodeBlock(CodeBlockKind::Fenced(fence)) => fence.len() + 3,
Tag::List(_) => 2,
Tag::Item => 2,
Tag::FootnoteDefinition(d) => d.len() + 3,
Tag::Table(_) => 4,
Tag::TableHead => 2,
Tag::TableRow => 3,
Tag::TableCell => 3,
Tag::Emphasis => 2,
Tag::Strong => 2,
Tag::Strikethrough => 2,
Tag::Link(_, u, t) => 4 + u.len() + t.len(),
Tag::Image(_, u, t) => 4 + u.len() + t.len(),
};
len
}
Event::End(_tag) => 4, }
}
pub(crate) fn sanitize_msgs_for_discord(msgs: Vec<String>) -> Vec<String> {
msgs.into_iter()
.flat_map(|m| split_msgs(m, 1800))
.map(|mut x| {
if x.len() > 1800 - 3 {
x.truncate(1800);
x.push_str("...");
}
x
})
.collect()
}
fn is_block_delimiter(t: &Tag) -> bool {
match t {
Tag::Paragraph => true,
Tag::Heading(_, _, _) => true,
Tag::BlockQuote => true,
Tag::CodeBlock(_) => true,
Tag::List(_) => true,
Tag::Item => false,
Tag::FootnoteDefinition(_) => true,
Tag::Table(_) => true,
Tag::TableHead => false,
Tag::TableRow => false,
Tag::TableCell => false,
Tag::Emphasis => false,
Tag::Strong => false,
Tag::Strikethrough => false,
Tag::Link(_, _, _) => false,
Tag::Image(_, _, _) => false,
}
}
fn split_msgs(msg: String, max_size: usize) -> Vec<String> {
if msg.len() <= max_size {
return vec![msg];
}
let mut buf = String::with_capacity(msg.len() + 128);
let mut msgs = vec![];
let mut options = Options::all();
options.remove(Options::ENABLE_SMART_PUNCTUATION);
let mut current_size = 0;
let mut state: Option<State> = None;
for event in Parser::new_ext(&msg, options) {
let event_size = md_event_size(&event);
if let Event::End(t) = &event {
if is_block_delimiter(t) & (current_size + event_size > max_size / 2)
|| (current_size + event_size > max_size * 2 / 3)
{
if let Some(state) = state {
state.finalize(&mut buf).unwrap();
msgs.push(buf.clone());
buf.clear();
}
current_size = 0;
state = None;
}
} else if current_size + event_size > max_size {
if let Some(state) = state {
state.finalize(&mut buf).unwrap();
msgs.push(buf.clone());
buf.clear();
}
current_size = 0;
state = None;
}
current_size += event_size;
state = cmark_resume(std::iter::once(event), &mut buf, state.take())
.unwrap()
.into();
}
if let Some(state) = state {
state.finalize(&mut buf).unwrap();
};
msgs.retain(|msg| !msg.trim().is_empty());
msgs
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use insta::assert_debug_snapshot;
use pulldown_cmark_to_cmark::cmark;
use super::*;
#[test]
fn estimate_size_in_markdown_one_by_one() {
let md = indoc! {
r#"This is a test message that is too long for Discord
# This is a section
This is the second paragraph
```python
print("Hello world")
```
This is the third paragraph
- This is a list
- This is another list item
- This is another list item
- This is another list item
## A subsection
-----------
| A | B |
|---|-----|
| 1 | 232 |
| 2 | 3 |
-----------
This is the fourth paragraph
# This is another section
This is the first paragraph of another section
"#}
.to_string();
let mut options = Options::all();
options.remove(Options::ENABLE_SMART_PUNCTUATION);
let mut normalized_md = String::with_capacity(md.len() + 128);
let state = cmark(Parser::new_ext(&md, options), &mut normalized_md);
if let Ok(state) = state {
state.finalize(&mut normalized_md).unwrap();
}
let mut options = Options::all();
options.remove(Options::ENABLE_SMART_PUNCTUATION);
let mut estimated_normalized_size = 0;
for event in Parser::new_ext(&normalized_md, options) {
estimated_normalized_size += md_event_size(&event);
}
assert!(
estimated_normalized_size >= normalized_md.len(),
"estimated_normalized_size: {}, normalized_md.len(): {}",
estimated_normalized_size,
normalized_md.len()
);
}
#[test]
fn test_split_msgs() {
let msg = indoc! {
r#"This is a test message that is too long for Discord
# This is a section
This is the second paragraph
```python
print("Hello world")
```
This is the third paragraph which is much longer than the other.
## A subsection
This is the fourth paragraph
# This is another section
This is the first paragraph of another section
"#}
.to_string();
let max_size = 100;
let msgs = split_msgs(msg, max_size);
assert_debug_snapshot!(msgs);
for msg in &msgs {
assert!(msg.len() <= max_size);
}
}
}