Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Tools/langtool/src/section.rs
5971 views
1
// Super simplified ini file processor.
2
// Doesn't even bother with understanding comments.
3
// Just understands section headings and
4
// keys and values, split by ' = '.
5
6
use regex::Regex;
7
8
#[derive(Debug, Clone)]
9
pub struct Section {
10
pub name: String,
11
pub title_line: String,
12
pub lines: Vec<String>,
13
}
14
15
pub fn split_line(line: &str) -> Option<(&str, &str)> {
16
let line = line.trim();
17
if let Some(pos) = line.find(" =") {
18
let value = &line[pos + 2..];
19
if value.is_empty() {
20
return None;
21
}
22
return Some((line[0..pos].trim(), value.trim()));
23
}
24
None
25
}
26
27
pub fn line_value(line: &str) -> Option<&str> {
28
split_line(line).map(|tuple| tuple.1)
29
}
30
31
impl Section {
32
pub fn apply_regex(&mut self, key: &str, pattern: &str, replacement: &str) {
33
let re = Regex::new(pattern).unwrap();
34
for line in self.lines.iter_mut() {
35
let prefix = if let Some(pos) = line.find(" =") {
36
&line[0..pos]
37
} else {
38
continue;
39
};
40
if prefix.eq(key) {
41
if let Some((_, value)) = split_line(line) {
42
let new_value = re.replace_all(value, replacement);
43
*line = format!("{} = {}", key, new_value);
44
}
45
}
46
}
47
}
48
49
pub fn remove_line(&mut self, key: &str) -> Option<String> {
50
let mut remove_index = None;
51
for (index, line) in self.lines.iter().enumerate() {
52
let prefix = if let Some(pos) = line.find(" =") {
53
&line[0..pos]
54
} else {
55
continue;
56
};
57
58
if prefix.eq(key) {
59
remove_index = Some(index);
60
break;
61
}
62
}
63
64
if let Some(remove_index) = remove_index {
65
Some(self.lines.remove(remove_index))
66
} else {
67
None
68
}
69
}
70
71
pub fn remove_linebreaks(&mut self, key: &str) {
72
for line in self.lines.iter_mut() {
73
let prefix = if let Some(pos) = line.find(" =") {
74
&line[0..pos]
75
} else {
76
continue;
77
};
78
if !prefix.trim().eq(key) {
79
continue;
80
}
81
// Escaped linebreaks.
82
*line = line.replace("\\n", " ");
83
}
84
}
85
86
pub fn get_line(&self, key: &str) -> Option<String> {
87
for line in self.lines.iter() {
88
let prefix = if let Some(pos) = line.find(" =") {
89
&line[0..pos]
90
} else {
91
continue;
92
};
93
94
if prefix.eq(key) {
95
return Some(line.clone());
96
}
97
}
98
None
99
}
100
101
pub fn insert_line_if_missing(&mut self, line: &str) -> bool {
102
let prefix = if let Some(pos) = line.find(" =") {
103
&line[0..pos + 2]
104
} else {
105
return false;
106
};
107
108
// Ignore comments when copying lines.
109
if prefix.starts_with('#') {
110
return false;
111
}
112
// Need to decide a policy for these.
113
if prefix.starts_with("translators") {
114
return false;
115
}
116
let prefix = prefix.to_owned();
117
118
for iter_line in &self.lines {
119
if iter_line.starts_with(&prefix) {
120
// Already have it
121
return false;
122
}
123
}
124
125
// Now try to insert it at an alphabetic-ish location.
126
let prefix = prefix.to_ascii_lowercase();
127
128
// Then, find a suitable insertion spot
129
for (i, iter_line) in self.lines.iter().enumerate() {
130
if iter_line.to_ascii_lowercase() > prefix {
131
println!("{}: Inserting line {line}", self.name);
132
self.lines.insert(i, line.to_owned());
133
return true;
134
}
135
}
136
137
for i in (0..self.lines.len()).rev() {
138
if self.lines[i].is_empty() {
139
continue;
140
}
141
println!("{}: Inserting line {line}", self.name);
142
self.lines.insert(i + 1, line.to_owned());
143
return true;
144
}
145
146
println!("{}: failed to insert {line}", self.name);
147
true
148
}
149
150
pub fn rename_key(&mut self, old: &str, new: &str) {
151
let prefix = old.to_owned() + " =";
152
let mut found_index = None;
153
for (index, line) in self.lines.iter().enumerate() {
154
if line.starts_with(&prefix) {
155
found_index = Some(index);
156
}
157
}
158
if let Some(index) = found_index {
159
let line = self.lines.remove(index);
160
let mut right_part = line.strip_prefix(&prefix).unwrap().to_string();
161
if right_part.trim() == old.trim() {
162
// Was still untranslated - replace the translation too.
163
right_part = format!(" {new}");
164
}
165
let line = new.to_owned() + " =" + &right_part;
166
self.insert_line_if_missing(&line);
167
} else {
168
let name = &self.name;
169
println!("rename_key: didn't find a line starting with {prefix} in section {name}");
170
}
171
}
172
173
fn capitalize_first_letter(s: &str) -> String {
174
if let Some(first_char) = s.chars().next() {
175
if first_char.is_ascii_alphabetic() {
176
let mut c = first_char.to_ascii_uppercase().to_string();
177
c.push_str(&s[first_char.len_utf8()..]);
178
return c;
179
}
180
}
181
s.to_string()
182
}
183
184
// split_key should take a line like:
185
// KeyName (KeyDescription) = TranslatedKeyName (TranslatedKeyDescription)
186
// and split it into:
187
// KeyName = TranslatedKeyName
188
// KeyDescription = TranslatedKeyDescription
189
// (Note: the first letter ONLY of description gets capitalized).
190
// Additional note, this must handle utf-8 unicode characters. Chinese characters for example
191
// can't be capitalized so we shouldn't do that to them. Also, parentheses around description shouldn't come
192
// along for the ride - get rid of them.
193
pub fn split_key(&mut self, key: &str) {
194
let prefix = key.to_owned() + " =";
195
let mut found_index = None;
196
for (index, line) in self.lines.iter().enumerate() {
197
if line.starts_with(&prefix) {
198
found_index = Some(index);
199
}
200
}
201
if let Some(index) = found_index {
202
let line = self.lines.remove(index);
203
let right_part = line.strip_prefix(&prefix).unwrap().to_string();
204
if let Some(pos) = key.find('(') {
205
let key_name = key[0..pos].trim();
206
let key_desc = key[pos+1..key.len()-1].trim();
207
if let Some(pos) = right_part.find('(') {
208
let value_name = right_part[0..pos].trim();
209
let value_desc = right_part[pos+1..right_part.len()-1].trim();
210
self.insert_line_if_missing(&format!("{} = {}", key_name, value_name));
211
self.insert_line_if_missing(&format!("{} = {}", Self::capitalize_first_letter(key_desc), Self::capitalize_first_letter(value_desc)));
212
} else {
213
println!("split_key: didn't find '(' in the value part {right_part} for key {key}. Leaving description untranslated.");
214
self.insert_line_if_missing(&format!("{} = {}", key_name, right_part.trim()));
215
self.insert_line_if_missing(&format!("{} = {}", Self::capitalize_first_letter(key_desc), Self::capitalize_first_letter(key_desc)));
216
}
217
} else {
218
println!("split_key: didn't find '(' in the key {key}");
219
}
220
} else {
221
let name = &self.name;
222
println!("split_key: didn't find a line starting with {prefix} in section {name}");
223
}
224
}
225
226
pub fn dupe_key(&mut self, old: &str, new: &str) {
227
let prefix = old.to_owned() + " =";
228
let mut found_index = None;
229
for (index, line) in self.lines.iter().enumerate() {
230
if line.starts_with(&prefix) {
231
found_index = Some(index);
232
}
233
}
234
if let Some(index) = found_index {
235
let line = self.lines.get(index).unwrap();
236
let mut right_part = line.strip_prefix(&prefix).unwrap().to_string();
237
if right_part.trim() == old.trim() {
238
// Was still untranslated - replace the translation too.
239
right_part = format!(" {new}");
240
}
241
let line = new.to_owned() + " =" + &right_part;
242
self.insert_line_if_missing(&line);
243
} else {
244
let name = &self.name;
245
println!("dupe_key: didn't find a line starting with {prefix} in section {name}");
246
}
247
}
248
249
pub fn sort(&mut self) {
250
self.lines.sort();
251
}
252
253
pub fn comment_out_lines_if_not_in(&mut self, other: &Section) {
254
// Brute force (O(n^2)). Bad but not a problem.
255
256
for line in &mut self.lines {
257
let prefix = if let Some(pos) = line.find(" =") {
258
&line[0..pos + 2]
259
} else {
260
// Keep non-key lines.
261
continue;
262
};
263
if prefix.starts_with("Font") || prefix.starts_with('#') {
264
continue;
265
}
266
if !other.lines.iter().any(|line| line.starts_with(prefix)) && !prefix.contains("URL") {
267
println!("Commenting out from {}: {line}", other.name);
268
// Comment out the line.
269
*line = "#".to_owned() + line;
270
}
271
}
272
}
273
274
pub fn remove_lines_if_not_in(&mut self, other: &Section) {
275
// Brute force (O(n^2)). Bad but not a problem.
276
277
self.lines.retain(|line| {
278
let prefix = if let Some(pos) = line.find(" =") {
279
&line[0..pos + 2]
280
} else {
281
// Keep non-key lines.
282
return true;
283
};
284
if prefix.starts_with("Font") || prefix.starts_with('#') {
285
return true;
286
}
287
288
// keeps the line if this expression returns true.
289
other.lines.iter().any(|line| line.starts_with(prefix))
290
});
291
}
292
293
pub fn get_lines_not_in(&self, other: &Section) -> Vec<String> {
294
let mut missing_lines = Vec::new();
295
// Brute force (O(n^2)). Bad but not a problem.
296
for line in &self.lines {
297
let prefix = if let Some(pos) = line.find(" =") {
298
&line[0..pos + 2]
299
} else {
300
// Keep non-key lines.
301
continue;
302
};
303
if prefix.starts_with("Font") || prefix.starts_with('#') {
304
continue;
305
}
306
307
// keeps the line if this expression returns true.
308
if !other.lines.iter().any(|line| line.starts_with(prefix)) {
309
missing_lines.push(line.clone());
310
}
311
}
312
missing_lines
313
}
314
315
pub fn get_keys_if_not_in(&mut self, other: &Section) -> Vec<String> {
316
let mut missing_lines = Vec::new();
317
// Brute force (O(n^2)). Bad but not a problem.
318
for line in &self.lines {
319
let prefix = if let Some(pos) = line.find(" =") {
320
&line[0..pos + 2]
321
} else {
322
// Keep non-key lines.
323
continue;
324
};
325
if prefix.starts_with("Font") || prefix.starts_with('#') {
326
continue;
327
}
328
329
// keeps the line if this expression returns true.
330
if !other.lines.iter().any(|line| line.starts_with(prefix)) {
331
missing_lines.push(prefix[0..prefix.len() - 2].trim().to_string());
332
}
333
}
334
missing_lines
335
}
336
337
// Returns true if the key was found and updated.
338
pub fn set_value(&mut self, key: &str, value: &str, comment: Option<&str>) -> bool {
339
let mut found_index = None;
340
for (index, line) in self.lines.iter().enumerate() {
341
let prefix = if let Some(pos) = line.find(" =") {
342
&line[0..pos]
343
} else {
344
continue;
345
};
346
347
if prefix.eq(key) {
348
found_index = Some(index);
349
break;
350
}
351
}
352
353
if let Some(found_index) = found_index {
354
self.lines[found_index] = match comment {
355
Some(c) => format!("{} = {} # {}", key, value, c),
356
None => format!("{} = {}", key, value),
357
};
358
true
359
} else {
360
false
361
}
362
}
363
364
pub fn get_value(&self, key: &str) -> Option<String> {
365
for line in &self.lines {
366
if let Some((ref_key, value)) = split_line(line) {
367
if key.eq(ref_key) {
368
// Found it!
369
// The value might have a comment starting with #, strip that before returning.
370
let value = value.split('#').next().unwrap().trim();
371
return Some(value.to_string());
372
}
373
}
374
}
375
None
376
}
377
}
378
379