use crate::parser::link_url::LinkDestination;
use super::hashtag_content_char_ranges::hashtag_content_char;
use super::Element;
use nom::{
bytes::{
complete::{tag, take, take_while, take_while1},
streaming::take_till1,
},
character::complete::char,
combinator::{peek, recognize, verify},
sequence::tuple,
AsChar, IResult, Offset, Slice,
};
use super::base_parsers::CustomError;
fn linebreak(input: &str) -> IResult<&str, char, CustomError<&str>> {
char('\n')(input)
}
fn hashtag(input: &str) -> IResult<&str, Element, CustomError<&str>> {
let (input, content) = recognize(tuple((char('#'), take_while1(hashtag_content_char))))(input)?;
Ok((input, Element::Tag(content)))
}
fn not_email_address_part_char(c: char) -> bool {
matches!(
c,
'@' | '\n'
| '\r'
| '\t'
| ' '
| ':'
| ';'
| '!'
| '?'
| ','
| '('
| ')'
| '{'
| '}'
| '['
| ']'
| '"'
)
}
fn email_address_part_char(c: char) -> bool {
!not_email_address_part_char(c)
}
fn email_intern(input: &str) -> IResult<&str, (), CustomError<&str>> {
let (input, _) = take_till1(not_email_address_part_char)(input)?;
let (input, _) = tag("@")(input)?;
let (input, _) = take_while1(email_address_part_char)(input)?;
Ok((input, ()))
}
pub(crate) fn email_address(input: &str) -> IResult<&str, Element, CustomError<&str>> {
let i = <&str>::clone(&input);
let i2 = <&str>::clone(&input);
let i3 = <&str>::clone(&input);
let (input, content) = match email_intern(i) {
Ok((mut remaining, _)) => {
let index = i2.offset(remaining);
let mut consumed = i2.slice(..index);
while let Some('.') = consumed.chars().last() {
let index = input.offset(remaining).saturating_sub(1);
consumed = i3.slice(..index);
remaining = input.slice(index..);
}
Ok((remaining, consumed))
}
Err(e) => Err(e),
}?;
if true {
Ok((input, Element::EmailAddress(content)))
} else {
Err(nom::Err::Error(CustomError::InvalidEmail))
}
}
pub(crate) fn fediverse_address_as_text(input: &str) -> IResult<&str, Element, CustomError<&str>> {
let (input, consumed) = recognize(tuple((tag("@"), email_address)))(input)?;
Ok((input, Element::Text(consumed)))
}
fn is_allowed_bot_cmd_suggestion_char(char: char) -> bool {
match char {
'@' | '\\' | '_' | '.' | '-' | '/' => true,
_ => char.is_alphanum(),
}
}
fn bot_command_suggestion(input: &str) -> IResult<&str, Element, CustomError<&str>> {
let (input, content) = recognize(tuple((
char('/'),
verify(take(1usize), |s: &str| {
s.chars().next().unwrap_or('.').is_alphabetic()
}),
verify(take_while(is_allowed_bot_cmd_suggestion_char), |s: &str| {
s.len() < 256
}),
)))(input)?;
if content.slice(1..).contains('/') {
Ok((input, Element::Text(content)))
} else {
Ok((input, Element::BotCommandSuggestion(content)))
}
}
pub(crate) fn parse_text_element(
input: &str,
prev_char: Option<char>,
) -> IResult<&str, Element, CustomError<&str>> {
if let Ok((i, elm)) = hashtag(input) {
Ok((i, elm))
} else if let Ok((i, elm)) = {
if prev_char == Some(' ') || prev_char.is_none() {
bot_command_suggestion(input)
} else {
Err(nom::Err::Error(
CustomError::<&str>::PrecedingWhitespaceMissing,
))
}
} {
Ok((i, elm))
} else if let Ok((i, elm)) = fediverse_address_as_text(input) {
Ok((i, elm))
} else if let Ok((i, elm)) = email_address(input) {
Ok((i, elm))
} else if let Ok((i, destination)) = LinkDestination::parse(input) {
Ok((i, Element::Link { destination }))
} else if let Ok((i, _)) = linebreak(input) {
Ok((i, Element::Linebreak))
} else {
Err(nom::Err::Error(CustomError::NoElement))
}
}
fn eat_text(input: &str) -> IResult<&str, (), CustomError<&str>> {
let mut remaining = input;
while !remaining.is_empty() {
let (remainder, taken) = take(1usize)(remaining)?;
remaining = remainder;
if peek(|input| parse_text_element(input, taken.chars().next()))(remaining).is_ok() {
break;
}
}
Ok((remaining, ()))
}
pub(crate) fn text(input: &str) -> IResult<&str, Element, CustomError<&str>> {
let (rest, content) = recognize(eat_text)(input)?;
Ok((rest, Element::Text(content)))
}