pub mod json;
pub mod resource;
use std::path::{Path, PathBuf};
use mf2_resource::{LineIndex, Span, ValueMap};
use crate::error::Result;
#[derive(Debug)]
pub struct SourceFile {
pub path: PathBuf,
pub text: String,
pub index: LineIndex,
}
impl SourceFile {
pub fn new(path: impl Into<PathBuf>, text: String) -> SourceFile {
let index = LineIndex::new(&text);
SourceFile {
path: path.into(),
text,
index,
}
}
pub fn read(path: impl Into<PathBuf>) -> Result<SourceFile> {
let path = path.into();
let text = std::fs::read_to_string(&path)
.map_err(|source| crate::error::Error::io(path.clone(), source))?;
Ok(SourceFile::new(path, text))
}
pub fn position(&self, at: u32) -> mf2_resource::Position {
self.index.position(&self.text, at)
}
}
#[derive(Debug)]
pub struct Record {
pub id: String,
pub source: String,
pub file: usize,
pub span: Span,
pub id_span: Span,
pub value_span: Span,
pub map: ValueMap,
pub comment: Option<String>,
pub meta: Vec<Property>,
}
impl Record {
pub fn source_span(&self, start: u32, end: u32) -> Span {
self.map.source_span(start, end).unwrap_or(self.value_span)
}
pub fn do_not_translate(&self) -> bool {
self.meta.iter().any(|p| p.name == "do-not-translate")
}
pub fn param(&self, name: &str) -> Option<&str> {
self.meta
.iter()
.filter(|p| p.name == "param")
.filter_map(|p| p.value.as_deref())
.find_map(|value| {
let rest = value.strip_prefix('$')?;
let (var, description) = split_param(rest);
(var == name).then_some(description)
})
}
}
fn split_param(rest: &str) -> (&str, &str) {
let end = rest
.find(|c: char| !mf2_resource::is_id_char(c))
.unwrap_or(rest.len());
let (var, description) = rest.split_at(end);
let description = description.trim_start();
let description = description.strip_prefix('-').unwrap_or(description);
(var, description.trim_start())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Property {
pub name: String,
pub value: Option<String>,
}
#[derive(Clone, Copy, Debug)]
pub struct Problem {
pub file: usize,
pub span: Span,
pub code: u16,
pub message: &'static str,
}
#[derive(Debug, Default)]
pub struct Loaded {
pub files: Vec<SourceFile>,
pub records: Vec<Record>,
pub problems: Vec<Problem>,
pub declared: Vec<Option<String>>,
}
impl Loaded {
pub fn file(&self, record: &Record) -> &SourceFile {
&self.files[record.file]
}
}
pub trait Loader {
fn name(&self) -> &'static str;
fn load(&self, path: &Path) -> Result<Loaded>;
}
pub fn for_path(path: &Path) -> Box<dyn Loader> {
if path.extension().is_some_and(|e| e == "json") {
Box::new(json::FlatJson)
} else {
Box::new(resource::Resources)
}
}
#[cfg(test)]
mod tests {
use super::{Property, Record, split_param};
use mf2_resource::{Span, ValueMap};
fn record(meta: Vec<Property>) -> Record {
Record {
id: "x".to_owned(),
source: String::new(),
file: 0,
span: Span { start: 0, end: 0 },
id_span: Span { start: 0, end: 0 },
value_span: Span { start: 0, end: 0 },
map: ValueMap::Empty,
comment: None,
meta,
}
}
#[test]
fn a_param_property_names_its_variable() {
assert_eq!(split_param("count - How many."), ("count", "How many."));
assert_eq!(split_param("name"), ("name", ""));
let r = record(vec![
Property {
name: "param".to_owned(),
value: Some("$count - How many people are in the room.".to_owned()),
},
Property {
name: "do-not-translate".to_owned(),
value: None,
},
]);
assert_eq!(r.param("count"), Some("How many people are in the room."));
assert_eq!(r.param("other"), None);
assert!(r.do_not_translate());
}
}