use super::*;
use trim_in_place::TrimInPlace;
#[derive(Default, Debug, Clone)]
pub struct CodeBlockError {
#[doc(hidden)]
pub __non_exhaustive: (),
}
impl std::fmt::Display for CodeBlockError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("couldn't find a valid code block")
}
}
impl std::error::Error for CodeBlockError {}
#[derive(Default, Debug, PartialEq, Eq, Clone, Hash)]
pub struct CodeBlock {
pub code: String,
pub language: Option<String>,
#[doc(hidden)]
pub __non_exhaustive: (),
}
impl std::fmt::Display for CodeBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"```{}\n{}\n```",
self.language.as_deref().unwrap_or(""),
&self.code
)
}
}
fn pop_from(args: &str) -> Result<(&str, CodeBlock), CodeBlockError> {
let args = args.trim_start();
let rest;
let mut code_block = if let Some(code_block) = args.strip_prefix("```") {
let code_block_end = code_block.find("```").ok_or(CodeBlockError::default())?;
rest = &code_block[(code_block_end + 3)..];
let mut code_block = &code_block[..code_block_end];
let mut language = None;
if let Some(first_newline) = code_block.find('\n') {
let is_valid = code_block[..first_newline]
.chars()
.all(|c| c.is_ascii_alphanumeric() || "+-._#".contains(c));
if is_valid {
language = Some(&code_block[..first_newline]);
code_block = &code_block[(first_newline + 1)..];
}
}
let code_block = code_block.trim_matches('\n');
CodeBlock {
code: code_block.to_owned(),
language: language.map(|x| x.to_owned()),
__non_exhaustive: (),
}
} else if let Some(code_line) = args.strip_prefix('`') {
let code_line_end = code_line.find('`').ok_or(CodeBlockError::default())?;
rest = &code_line[(code_line_end + 1)..];
let code_line = &code_line[..code_line_end];
CodeBlock {
code: code_line.to_owned(),
language: None,
__non_exhaustive: (),
}
} else {
return Err(CodeBlockError::default());
};
if code_block.code.is_empty() {
Err(CodeBlockError::default())
} else {
code_block.code.trim_end_matches_in_place('\u{200a}');
Ok((rest, code_block))
}
}
#[async_trait::async_trait]
impl<'a> PopArgument<'a> for CodeBlock {
async fn pop_from(
args: &'a str,
attachment_index: usize,
_: &serenity::Context,
_: &serenity::Message,
) -> Result<(&'a str, usize, Self), (Box<dyn std::error::Error + Send + Sync>, Option<String>)>
{
let (a, b) = pop_from(args).map_err(|e| (e.into(), None))?;
Ok((a, attachment_index, b))
}
}
#[cfg(test)]
#[test]
fn test_pop_code_block() {
for &(string, code, language) in &[
("`hello world`", "hello world", None),
("` `", " ", None),
("``` hi ```", " hi ", None),
("```rust```", "rust", None),
("```rust\nhi```", "hi", Some("rust")),
("```rust hi```", "rust hi", None),
("```rust\n\n\n\n\nhi\n\n\n\n```", "hi", Some("rust")),
("```+__....-.+.++\nhi\n```", "hi", Some("+__....-.+.++")),
("```+__.:...-.+.++\nhi\n```", "+__.:...-.+.++\nhi", None),
(
"```#![feature(never_type)]\nfn uwu(_: &!) {}\n```",
"#![feature(never_type)]\nfn uwu(_: &!) {}",
None,
),
("```c#\nusing System;\n```", "using System;", Some("c#")),
] {
assert_eq!(
pop_from(string).unwrap().1,
CodeBlock {
code: code.into(),
language: language.map(|x| x.into()),
__non_exhaustive: (),
}
);
}
assert!(pop_from("").is_err());
assert!(pop_from("''").is_err());
assert!(pop_from("``").is_err());
assert!(pop_from("``````").is_err());
}