use super::js;
use super::position::PositionIndex;
use super::{Path, Position, heuristics};
pub(crate) fn extract(content: &str) -> Vec<Path> {
if js::is_blank(content) {
return Vec::new();
}
let Ok(parsed) = content.parse::<::toml::Table>() else {
return Vec::new();
};
let index = PositionIndex::new(content);
let mut locator = Locator::new(content, &index);
let mut paths = Vec::new();
walk(&::toml::Value::Table(parsed), &mut paths, &mut locator);
paths
}
fn walk(value: &::toml::Value, paths: &mut Vec<Path>, locator: &mut Locator) {
match value {
::toml::Value::String(text) => {
if heuristics::is_path_like(text) {
paths.push(Path {
value: text.clone(),
kind: heuristics::classify_path_type(text),
position: locator.locate(text),
context: "TOML value".to_string(),
});
}
}
::toml::Value::Array(items) => {
for item in items {
walk(item, paths, locator);
}
}
::toml::Value::Table(table) => {
for (key, value) in table {
if heuristics::is_path_like(key) {
paths.push(Path {
value: key.clone(),
kind: heuristics::classify_path_type(key),
position: locator.locate(key),
context: "TOML key".to_string(),
});
}
walk(value, paths, locator);
}
}
_ => {}
}
}
struct Locator<'a> {
content: &'a str,
index: &'a PositionIndex<'a>,
search_from: usize,
}
impl<'a> Locator<'a> {
fn new(content: &'a str, index: &'a PositionIndex<'a>) -> Self {
Self {
content,
index,
search_from: 0,
}
}
fn locate(&mut self, value: &str) -> Position {
let escaped = value.replace('\\', r"\\");
for candidate in [value, escaped.as_str()] {
if let Some(offset) = self.content[self.search_from..]
.find(candidate)
.map(|at| at + self.search_from)
{
self.search_from = offset + candidate.len();
return self.index.at(offset);
}
if let Some(anywhere) = self.content.find(candidate) {
return self.index.at(anywhere);
}
}
Position { line: 1, column: 1 }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::PathType;
#[test]
fn a_blank_document_yields_nothing() {
assert!(extract("").is_empty());
assert!(extract(" \n ").is_empty());
}
#[test]
fn a_document_that_does_not_parse_yields_nothing() {
assert!(extract("this is [not toml").is_empty());
}
#[test]
fn values_and_keys_both_count() {
let paths = extract("[\"./key.sh\"]\nvalue = \"./value.sh\"\n");
assert_eq!(paths.len(), 2);
assert_eq!(paths[0].value, "./key.sh");
assert_eq!(paths[0].context, "TOML key");
assert_eq!(paths[1].value, "./value.sh");
assert_eq!(paths[1].context, "TOML value");
}
#[test]
fn arrays_are_walked() {
let paths = extract("files = [\"./a.ts\", \"./b.ts\"]\n");
assert_eq!(paths.len(), 2);
assert_eq!(paths[1].value, "./b.ts");
}
#[test]
fn tables_iterate_in_document_order() {
let paths = extract("[z]\na = \"./first.ts\"\n\n[a]\nb = \"./second.ts\"\n");
assert_eq!(paths[0].value, "./first.ts");
assert_eq!(paths[0].position.line, 2);
assert_eq!(paths[1].value, "./second.ts");
assert_eq!(paths[1].position.line, 5);
}
#[test]
fn repeated_values_land_on_successive_occurrences() {
let paths = extract("a = \"./x.ts\"\nb = \"./x.ts\"\n");
assert_eq!(paths[0].position.line, 1);
assert_eq!(paths[1].position.line, 2);
}
#[test]
fn a_windows_path_is_located_through_its_escaped_form() {
let paths = extract(r#"p = "C:\\Temp\\x.txt""#);
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].value, r"C:\Temp\x.txt");
assert_eq!(paths[0].kind, PathType::Absolute);
assert_eq!(paths[0].position.column, 6);
}
#[test]
fn a_literal_string_is_located_directly() {
let paths = extract(r"p = 'C:\Temp\x.txt'");
assert_eq!(paths[0].value, r"C:\Temp\x.txt");
assert_eq!(paths[0].position.column, 6);
}
#[test]
fn an_unlocatable_value_falls_back_to_the_first_position() {
let paths = extract("p = \"\"\"\n./spread\\\n /out.ts\"\"\"\n");
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].position, Position { line: 1, column: 1 });
}
#[test]
fn non_string_values_are_skipped() {
let paths = extract("a = 1\nb = true\nc = 1979-05-27\nd = \"./x.ts\"\n");
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].value, "./x.ts");
}
}