1use std::collections::BTreeMap;
2use std::hash::{DefaultHasher, Hash, Hasher};
3use std::ops::Range;
4
5use crate::lex::{Kind, Lexed};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Table {
9 pub path: Vec<String>,
10 pub index: Option<usize>,
11 pub header: Range<usize>,
12}
13
14impl Table {
15 pub fn same(&self, other: &Table) -> bool {
16 self.path == other.path && self.index == other.index
17 }
18}
19
20fn header(text: &str) -> Option<(Vec<String>, bool)> {
21 let (inner, array) = match text.strip_prefix("[[").and_then(|t| t.strip_suffix("]]")) {
22 Some(inner) => (inner, true),
23 None => (text.strip_prefix('[')?.strip_suffix(']')?, false),
24 };
25 let keys = toml_edit::Key::parse(inner.trim()).ok()?;
26 Some((
27 keys.iter().map(|key| key.get().to_string()).collect(),
28 array,
29 ))
30}
31
32type Entry = Option<(Vec<String>, Option<usize>)>;
33
34fn headers(lexed: &Lexed) -> Vec<Range<usize>> {
35 lexed
36 .lines()
37 .iter()
38 .filter_map(|line| {
39 let token = line
40 .tokens
41 .iter()
42 .find(|token| token.kind == Kind::Header)?;
43 Some(token.range.start + line.start..token.range.end + line.start)
44 })
45 .collect()
46}
47
48fn entries(text: &str, headers: &[Range<usize>]) -> Vec<Entry> {
49 let mut counts: BTreeMap<Vec<String>, usize> = BTreeMap::new();
50 headers
51 .iter()
52 .map(|range| {
53 let (path, array) = header(&text[range.clone()])?;
54 counts.retain(|known, _| !(known.len() > path.len() && known.starts_with(&path)));
55 let index = array.then(|| {
56 let count = counts.entry(path.clone()).or_default();
57 *count += 1;
58 *count - 1
59 });
60 Some((path, index))
61 })
62 .collect()
63}
64
65#[derive(Debug, Default)]
66pub struct TableCache {
67 key: Option<u64>,
68 entries: Vec<Entry>,
69 parsed: u64,
70}
71
72impl TableCache {
73 pub fn at(&mut self, lexed: &Lexed, byte: usize) -> Option<Table> {
74 let text = lexed.text();
75 let headers = headers(lexed);
76 let mut hasher = DefaultHasher::new();
77 for range in &headers {
78 text[range.clone()].hash(&mut hasher);
79 }
80 let key = hasher.finish();
81 if self.key != Some(key) {
82 self.entries = entries(text, &headers);
83 self.key = Some(key);
84 self.parsed += 1;
85 }
86 let line = lexed.line_of(byte.min(text.len()));
87 let end = lexed
88 .lines()
89 .get(line + 1)
90 .map_or(usize::MAX, |next| next.start);
91 let before = headers.partition_point(|range| range.start < end);
92 (0..before).rev().find_map(|index| {
93 let (path, at) = self.entries[index].clone()?;
94 Some(Table {
95 path,
96 index: at,
97 header: headers[index].clone(),
98 })
99 })
100 }
101
102 pub fn parsed(&self) -> u64 {
103 self.parsed
104 }
105}
106
107pub fn tables(lexed: &Lexed) -> Vec<Table> {
108 let headers = headers(lexed);
109 entries(lexed.text(), &headers)
110 .into_iter()
111 .zip(headers)
112 .filter_map(|(entry, header)| {
113 let (path, index) = entry?;
114 Some(Table {
115 path,
116 index,
117 header,
118 })
119 })
120 .collect()
121}
122
123pub fn table_at(lexed: &Lexed, byte: usize) -> Option<Table> {
124 TableCache::default().at(lexed, byte)
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 const TEXT: &str = "top = 1\n[[object]]\nname = \"a\"\n[[object]]\nname = \"b\"\n[[object.part]]\nx = 1\n[[object]]\nname = \"c\"\n[[object.part]]\nx = 2\n[materials.\"red one\"]\nshine = 0.5\n";
132
133 fn at(needle: &str) -> Option<Table> {
134 table_at(&Lexed::new(TEXT), TEXT.find(needle).unwrap())
135 }
136
137 #[test]
138 fn each_header_names_its_table_and_its_place_in_an_array() {
139 let found = tables(&Lexed::new(TEXT));
140 let summary: Vec<(String, Option<usize>)> = found
141 .iter()
142 .map(|table| (table.path.join("."), table.index))
143 .collect();
144 assert_eq!(
145 summary,
146 vec![
147 ("object".into(), Some(0)),
148 ("object".into(), Some(1)),
149 ("object.part".into(), Some(0)),
150 ("object".into(), Some(2)),
151 ("object.part".into(), Some(0)),
152 ("materials.red one".into(), None),
153 ]
154 );
155 assert_eq!(&TEXT[found[5].header.clone()], "[materials.\"red one\"]");
156 }
157
158 #[test]
159 fn a_byte_belongs_to_the_table_above_it() {
160 assert_eq!(at("top"), None);
161 assert_eq!(at("name = \"c\"").unwrap().index, Some(2));
162 assert_eq!(at("x = 2").unwrap().path, vec!["object", "part"]);
163 let header = TEXT.find("[[object]]\nname = \"b\"").unwrap();
164 assert_eq!(
165 table_at(&Lexed::new(TEXT), header + 3).unwrap().index,
166 Some(1)
167 );
168 assert_eq!(table_at(&Lexed::new(TEXT), header).unwrap().index, Some(1));
169 assert_eq!(at("shine").unwrap().path, vec!["materials", "red one"]);
170 assert!(at("shine").unwrap().same(&at("shine").unwrap()));
171 }
172
173 #[test]
174 fn the_cache_parses_headers_again_only_when_one_changes() {
175 let mut cache = TableCache::default();
176 let typed = TEXT.replace("x = 2", "x = 22");
177 let renamed = TEXT.replace("[materials.\"red one\"]", "[materials.blue]");
178 for (text, needle) in [(TEXT, "x = 2"), (&typed, "x = 22"), (&renamed, "shine")] {
179 let lexed = Lexed::new(text);
180 let at = text.find(needle).unwrap();
181 assert_eq!(cache.at(&lexed, at), table_at(&lexed, at));
182 }
183 assert_eq!(cache.parsed(), 2);
184 }
185}