use logos::Logos;
use crate::highlight::{Span, SyntaxKind};
#[derive(Logos, Debug, PartialEq)]
enum Entity {
#[regex(r"https?://[^\s<>()\[\]]+", priority = 3)]
Url,
#[regex(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", priority = 3)]
Email,
#[regex(r"@[A-Za-z0-9_][A-Za-z0-9_.-]*")]
Mention,
#[regex(r"#[A-Za-z][A-Za-z0-9_/-]*")]
Tag,
}
impl Entity {
fn kind(&self) -> SyntaxKind {
match self {
Entity::Url => SyntaxKind::Url,
Entity::Email => SyntaxKind::Email,
Entity::Mention => SyntaxKind::Mention,
Entity::Tag => SyntaxKind::Tag,
}
}
}
pub fn entities(text: &str) -> Vec<Span> {
let mut spans = Vec::new();
let mut lex = Entity::lexer(text);
while let Some(tok) = lex.next() {
if let Ok(entity) = tok {
spans.push(Span {
range: lex.span(),
kind: entity.kind(),
});
}
}
spans
}
#[cfg(test)]
mod tests {
use super::*;
fn pairs(text: &str) -> Vec<(SyntaxKind, &str)> {
entities(text)
.into_iter()
.map(|s| (s.kind, &text[s.range]))
.collect()
}
#[test]
fn matches_url_mention_tag() {
let got = pairs("see https://ex.com/p and @ada about #rust");
assert!(got.contains(&(SyntaxKind::Url, "https://ex.com/p")), "{got:?}");
assert!(got.contains(&(SyntaxKind::Mention, "@ada")), "{got:?}");
assert!(got.contains(&(SyntaxKind::Tag, "#rust")), "{got:?}");
}
#[test]
fn email_beats_mention() {
let got = pairs("mail ada@example.com now");
assert!(
got.iter()
.any(|(k, s)| *k == SyntaxKind::Email && *s == "ada@example.com"),
"{got:?}"
);
assert!(!got.iter().any(|(k, _)| *k == SyntaxKind::Mention), "{got:?}");
}
#[test]
fn plain_text_has_no_entities() {
assert!(entities("just some words, nothing special here").is_empty());
}
#[test]
fn tag_needs_a_letter_first() {
let t = "item #1 and #todo";
let got = pairs(t);
assert_eq!(got, vec![(SyntaxKind::Tag, "#todo")], "{got:?}");
}
}