#![forbid(unsafe_code)]
#![deny(missing_docs)]
use std::sync::LazyLock;
use serde::Serialize;
#[derive(Serialize, Clone, Debug)]
pub enum Fragment<'a> {
Raw(&'a str),
Regex(#[serde(serialize_with = "Fragment::serialize_regex")] regex::bytes::Regex),
}
impl<'a> Fragment<'a> {
fn serialize_regex<S>(regex: ®ex::bytes::Regex, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let pattern = regex.as_str();
serializer.serialize_str(pattern)
}
pub fn new(fragment: &'a str) -> Self {
if !fragment.bytes().any(|c| c.is_ascii_whitespace()) {
Fragment::Raw(fragment)
} else {
let escaped = regex::escape(fragment);
#[allow(clippy::unwrap_used)]
static WHITESPACE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\s+").unwrap());
let regex = WHITESPACE.replace_all(&escaped, "\\s+");
Fragment::Regex(
regex::bytes::Regex::new(®ex)
.expect("internal error: failed to compile fragment regex"),
)
}
}
}
impl<'doc> From<&'doc str> for Fragment<'doc> {
fn from(fragment: &'doc str) -> Self {
Self::new(fragment)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub fn adjust(self, bias: usize) -> Self {
Self {
start: self.start + bias,
end: self.end + bias,
}
}
pub fn as_range(&self) -> std::ops::Range<usize> {
self.start..self.end
}
}
impl From<std::ops::Range<usize>> for Span {
fn from(range: std::ops::Range<usize>) -> Self {
Self {
start: range.start,
end: range.end,
}
}
}
#[derive(Serialize, Clone, Debug)]
pub struct Subfeature<'a> {
pub after: usize,
pub fragment: Fragment<'a>,
}
impl<'a> Subfeature<'a> {
pub fn new(after: usize, fragment: impl Into<Fragment<'a>>) -> Self {
Self {
after,
fragment: fragment.into(),
}
}
pub fn locate_within(&self, feature: &str) -> Option<Span> {
let feature = feature.as_bytes();
let bias = self.after;
let focus = &feature[bias..];
match &self.fragment {
Fragment::Raw(fragment) => {
memchr::memmem::find(focus, fragment.as_bytes()).map(|start| {
let end = start + fragment.len();
Span::from(start..end).adjust(bias)
})
}
Fragment::Regex(regex) => regex
.find(focus)
.map(|m| Span::from(m.range()).adjust(bias)),
}
}
}
#[cfg(test)]
mod tests {
use crate::Fragment;
#[test]
fn test_fragment_from_context() {
for (ctx, expected) in &[
("foo.bar", "foo.bar"),
("foo . bar", r"foo\s+\.\s+bar"),
("foo['bar']", "foo['bar']"),
("foo [\n'bar'\n]", r"foo\s+\[\s+'bar'\s+\]"),
] {
match Fragment::from(*ctx) {
Fragment::Raw(actual) => assert_eq!(actual, *expected),
Fragment::Regex(actual) => assert_eq!(actual.as_str(), *expected),
}
}
}
}