#![forbid(unsafe_code)]
mod class;
mod linear;
mod nfa;
pub mod parser;
mod ucd;
mod unicode;
mod vm;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::Arc;
use linear::LinearMatcher;
use nfa::Program;
pub use parser::Dialect;
pub use ucd::UnicodeVersion;
pub use unicode::with_unicode_version;
thread_local! {
static COMPILE_CACHE: RefCell<HashMap<(String, Dialect, UnicodeVersion), Arc<Pattern>>>
= RefCell::new(HashMap::new());
}
pub fn compile_with_cached(
src: &str, dialect: Dialect,
) -> Result<Arc<Pattern>, String> {
let version = unicode::current_ucd_version();
let key = (src.to_string(), dialect, version);
if let Some(hit) = COMPILE_CACHE.with(|c| c.borrow().get(&key).cloned()) {
return Ok(hit);
}
let pat = Pattern::compile_with(src, dialect)?;
let arc = Arc::new(pat);
COMPILE_CACHE.with(|c| {
c.borrow_mut().insert(key, arc.clone());
});
Ok(arc)
}
pub struct Pattern {
src: String,
body: Body,
}
enum Body {
Linear(LinearMatcher),
Full(Program),
}
impl Pattern {
pub fn compile(src: &str) -> Result<Self, String> {
Self::compile_with(src, Dialect::Xsd)
}
pub fn compile_with(src: &str, dialect: Dialect) -> Result<Self, String> {
let ast = parser::parse_with(src, dialect)?;
let body = match dialect {
Dialect::Xsd => match LinearMatcher::try_build(&ast) {
Some(lm) => Body::Linear(lm),
None => Body::Full(nfa::compile(&ast)?),
},
Dialect::Xpath | Dialect::Xpath20 => Body::Full(nfa::compile(&ast)?),
};
Ok(Self { src: src.into(), body })
}
#[doc(hidden)]
pub fn compile_nfa_only(src: &str) -> Result<Self, String> {
let ast = parser::parse(src)?;
Ok(Self { src: src.into(), body: Body::Full(nfa::compile(&ast)?) })
}
pub fn is_match(&self, s: &str) -> bool {
match &self.body {
Body::Linear(m) => m.is_match(s),
Body::Full(p) => vm::is_match(p, s),
}
}
pub fn find_match(&self, s: &str) -> bool {
match &self.body {
Body::Linear(_) => panic!(
"find_match called on a Linear-compiled Pattern; \
compile with Dialect::Xpath for find semantics"
),
Body::Full(p) => vm::find_match(p, s),
}
}
pub fn find_iter(&self, input: &str) -> Vec<(usize, usize)> {
let prog = match &self.body {
Body::Full(p) => p,
Body::Linear(_) => panic!(
"find_iter called on a Linear-compiled Pattern; \
compile with Dialect::Xpath for find-style iteration"
),
};
let total_chars = input.chars().count();
let mut out: Vec<(usize, usize)> = Vec::new();
let mut pos: usize = 0;
let mut char_pos: usize = 0;
while pos <= input.len() {
let slice = &input[pos..];
match vm::leftmost_match_at_start(prog, slice, char_pos, total_chars) {
Some(len) if len > 0 => {
out.push((pos, pos + len));
char_pos += input[pos..pos + len].chars().count();
pos += len;
}
Some(_) => {
out.push((pos, pos));
if pos == input.len() { break; }
let c = input[pos..].chars().next().unwrap();
pos += c.len_utf8();
char_pos += 1;
}
None => {
if pos == input.len() { break; }
let c = input[pos..].chars().next().unwrap();
pos += c.len_utf8();
char_pos += 1;
}
}
}
out
}
pub fn src(&self) -> &str { &self.src }
}
impl Clone for Pattern {
fn clone(&self) -> Self {
let body = match &self.body {
Body::Linear(m) => Body::Linear(m.clone()),
Body::Full(p) => Body::Full(p.clone()),
};
Self { src: self.src.clone(), body }
}
}
impl std::fmt::Debug for Pattern {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Pattern").field("src", &self.src).finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caret_dollar_are_anchors_in_both_xpath_dialects() {
for d in [Dialect::Xpath, Dialect::Xpath20] {
let re = Pattern::compile_with("^a$", d).unwrap();
assert!(re.is_match("a"), "{d:?}: `^a$` should anchor-match \"a\"");
assert!(!re.is_match("^a$"), "{d:?}: `^`/`$` must be anchors, not literals");
let g = Pattern::compile_with("^(a+)$", d).unwrap();
assert!(g.is_match("aaa"), "{d:?}: `^(a+)$` should match \"aaa\"");
assert!(!g.is_match("baaa"), "{d:?}: anchored, so a leading `b` fails");
assert!(re.find_match("a"), "{d:?}: find `^a$` in \"a\"");
assert!(!re.find_match("xa"), "{d:?}: `^` pins to start");
let tail = Pattern::compile_with("a$", d).unwrap();
assert!(tail.find_match("ba"), "{d:?}: `a$` matches the tail of \"ba\"");
assert!(!tail.find_match("ab"), "{d:?}: `$` pins to end");
}
}
#[test]
fn caret_dollar_are_literals_in_xsd_dialect() {
let re = Pattern::compile_with("^a$", Dialect::Xsd).unwrap();
assert!(re.is_match("^a$"), "XSD: `^`/`$` are literal characters");
assert!(!re.is_match("a"), "XSD: the literal `^`/`$` must be present");
}
}