pub(crate) mod format;
pub(crate) mod heuristics;
pub(crate) mod parse;
pub(crate) mod position;
pub(crate) mod time;
#[cfg(test)]
pub(crate) mod corpus;
pub(crate) use format::{SUPPORTED_FORMATS, resolve_format};
pub(crate) use heuristics::Found;
pub(crate) fn extract(content: &str, language: &str, year: i64) -> Vec<Found> {
let patterns = heuristics::patterns_for(language);
match language {
"xml" => heuristics::scan(&mask_xml_comments(content), content, &patterns, year),
"json" | "yaml" | "csv" | "log" | "plaintext" | "javascript" | "typescript" | "html" => {
heuristics::scan(content, content, &patterns, year)
}
_ => Vec::new(),
}
}
fn mask_xml_comments(content: &str) -> String {
let mut out = String::with_capacity(content.len());
let bytes = content.as_bytes();
let mut index = 0;
while index < content.len() {
if bytes[index..].starts_with(b"<!--") {
let rest = &content[index..];
let end = rest.find("-->").map_or(content.len(), |at| index + at + 3);
for character in content[index..end].chars() {
if character == '\n' {
out.push('\n');
} else {
for _ in 0..character.len_utf8() {
out.push(' ');
}
}
}
index = end;
} else {
let character = content[index..]
.chars()
.next()
.expect("index is on a character boundary");
out.push(character);
index += character.len_utf8();
}
}
debug_assert_eq!(
out.len(),
content.len(),
"masking changed the document's byte length"
);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn values(content: &str, language: &str) -> Vec<String> {
extract(content, language, 2026)
.into_iter()
.map(|found| found.value)
.collect()
}
#[test]
fn every_format_with_an_extractor_reads_a_bare_date() {
for language in [
"json",
"yaml",
"csv",
"xml",
"log",
"plaintext",
"javascript",
"typescript",
"html",
] {
assert_eq!(values("2024-01-15", language), ["2024-01-15"], "{language}");
}
}
#[test]
fn a_language_with_no_extractor_reads_nothing() {
assert!(values("2024-01-15", "rust").is_empty());
}
#[test]
fn a_date_in_an_xml_comment_is_skipped() {
assert!(values("<!-- 2024-01-15 -->", "xml").is_empty());
assert_eq!(
values("<!-- 1999-12-31 --><a>2024-01-15</a>", "xml"),
["2024-01-15"]
);
}
#[test]
fn an_unclosed_comment_swallows_the_rest() {
assert!(values("<!-- 2024-01-15", "xml").is_empty());
}
#[test]
fn masking_preserves_the_byte_length() {
for content in [
"<!-- café -->",
"<!-- Résumé — año -->",
"<!--\n2024\n-->",
"<a>é</a><!-- ✓ -->",
] {
assert_eq!(
mask_xml_comments(content).len(),
content.len(),
"{content:?}"
);
}
}
#[test]
fn masking_keeps_the_lines_it_spans() {
let masked = mask_xml_comments("<!--\na\nb\n-->x");
assert_eq!(masked.lines().count(), 4);
assert!(masked.ends_with('x'));
}
#[test]
fn a_date_after_a_multibyte_comment_keeps_its_column() {
let found = extract("<a><!-- café — naïve -->2024-01-15</a>", "xml", 2026);
assert_eq!(found.len(), 1);
assert_eq!((found[0].line, found[0].column), (1, 25));
}
}