1use std::iter::Peekable;
2use std::mem::take;
3use std::str::Chars;
4
5use crate::data::localization::{Language, LocaEntry, LocaValue, MacroValue};
6use crate::datatype::{Code, CodeArg, CodeChain};
7use crate::fileset::FileEntry;
8use crate::game::Game;
9use crate::parse::cob::Cob;
10use crate::parse::ignore::{IgnoreFilter, IgnoreSize, parse_comment};
11use crate::report::register_ignore_filter;
12use crate::report::{ErrorKey, untidy, warn};
13use crate::token::{Loc, Token};
14
15fn is_key_char(c: char) -> bool {
16 c.is_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '\''
17}
18
19fn is_code_char(c: char) -> bool {
22 c.is_alphanumeric() || c == '_' || c == '\''
23}
24
25#[derive(Clone, Debug)]
26struct LocaParser {
27 loc: Loc,
28 offset: usize,
29 content: &'static str,
30 chars: Peekable<Chars<'static>>,
31 language: Language,
32 expecting_language: bool,
33 loca_end: usize,
34 value: Vec<LocaValue>,
35 pending_line_ignores: Vec<IgnoreFilter>,
36 active_range_ignores: Vec<(u32, IgnoreFilter)>,
37}
38
39impl LocaParser {
40 fn new(entry: &FileEntry, content: &'static str, lang: Language) -> Self {
41 let mut chars = content.chars().peekable();
42 let mut offset = 0;
43
44 let mut loc = Loc::from(entry);
45 loc.column = 1; if chars.peek() == Some(&'\u{feff}') {
49 offset += '\u{feff}'.len_utf8();
50 loc.column += 1;
51 chars.next();
52
53 if chars.peek() == Some(&'\u{feff}') {
54 loc.line = 1;
56 let msg = "double BOM in localization file";
57 let info = "This will make the game engine skip the whole file.";
58 warn(ErrorKey::Encoding).strong().msg(msg).info(info).loc(loc).push();
59 offset += '\u{feff}'.len_utf8();
60 loc.column += 1;
61 chars.next();
62 }
63 } else {
64 warn(ErrorKey::Encoding).msg("Expected UTF-8 BOM encoding").abbreviated(loc).push();
65 }
66
67 loc.line = 1;
69
70 LocaParser {
71 loc,
72 offset,
73 content,
74 chars,
75 language: lang,
76 expecting_language: true,
77 value: Vec::new(),
78 loca_end: 0,
79 pending_line_ignores: Vec::new(),
80 active_range_ignores: Vec::new(),
81 }
82 }
83
84 fn next_char(&mut self) {
85 if let Some(c) = self.chars.next() {
87 self.offset += c.len_utf8();
88 if c == '\n' {
89 self.loc.line += 1;
90 self.loc.column = 1;
91 } else {
92 self.loc.column += 1;
93 }
94 }
95 }
96
97 fn skip_whitespace(&mut self) {
98 while let Some(c) = self.chars.peek() {
99 if c.is_whitespace() {
100 self.next_char();
101 } else {
102 break;
103 }
104 }
105 }
106
107 fn skip_linear_whitespace(&mut self) {
108 while let Some(c) = self.chars.peek() {
109 if c.is_whitespace() && *c != '\n' {
110 self.next_char();
111 } else {
112 break;
113 }
114 }
115 }
116
117 fn skip_line(&mut self) {
118 while let Some(&c) = self.chars.peek() {
119 if c == '\n' {
120 break;
121 }
122 self.next_char();
123 }
124 self.next_char(); }
126
127 #[allow(clippy::unnecessary_wraps)]
130 fn error_line(&mut self, key: Token) -> Option<LocaEntry> {
131 self.skip_line();
132 Some(LocaEntry::new(key, LocaValue::Error, None))
133 }
134
135 fn get_key(&mut self) -> Token {
136 let loc = self.loc;
137 let start_offset = self.offset;
138 while let Some(c) = self.chars.peek() {
139 if is_key_char(*c) {
140 self.next_char();
141 } else {
142 break;
143 }
144 }
145 let s = &self.content[start_offset..self.offset];
146 Token::from_static_str(s, loc)
147 }
148
149 fn unexpected_char(&mut self, expected: &str) {
150 match self.chars.peek() {
151 None => warn(ErrorKey::Localization)
152 .msg(format!("Unexpected end of file, {expected}"))
153 .loc(self.loc)
154 .push(),
155 Some(c) => warn(ErrorKey::Localization)
156 .msg(format!("Unexpected character `{c}`, {expected}"))
157 .loc(self.loc)
158 .push(),
159 }
160 }
161
162 fn find_dquote(&self) -> Option<usize> {
164 let mut offset = self.offset;
165 let mut dquote_offset = None;
166 for c in self.chars.clone() {
167 if c == '"' {
168 dquote_offset = Some(offset);
169 } else if c == '\n' {
170 return dquote_offset;
171 }
172 offset += c.len_utf8();
173 }
174 dquote_offset
175 }
176
177 fn parse_format(&mut self) -> Option<Token> {
178 (self.chars.peek() == Some(&'|')).then(|| {
179 self.next_char(); let loc = self.loc;
181 let mut text = String::new();
182 while let Some(&c) = self.chars.peek() {
183 if c == '$' || c == ']' || c == '\n' {
184 break;
185 }
186 text.push(c);
187 self.next_char();
188 }
189 Token::new(&text, loc)
190 })
191 }
192
193 fn line_has_macros(&self) -> bool {
194 for c in self.chars.clone() {
195 if c == '\n' {
196 return false;
197 } else if c == '$' {
198 return true;
199 }
200 }
201 false
202 }
203
204 fn parse_macros(&mut self) {
205 let mut v = Vec::new();
207 let mut loc = self.loc;
208 let mut offset = self.offset;
209 while let Some(&c) = self.chars.peek() {
210 if c == '$' {
211 let s = &self.content[offset..self.offset];
212 v.push(MacroValue::Text(Token::from_static_str(s, loc)));
213
214 if let Some(mv) = self.parse_keyword() {
215 v.push(mv);
216 } else {
217 self.value.push(LocaValue::Error);
218 return;
219 }
220 loc = self.loc;
221 offset = self.offset;
222 } else if c == '"' && self.offset == self.loca_end {
223 let s = &self.content[offset..self.offset];
224 v.push(MacroValue::Text(Token::from_static_str(s, loc)));
225 self.value.push(LocaValue::Macro(v));
226 self.next_char();
227 return;
228 } else {
229 self.next_char();
230 }
231 }
232 let s = &self.content[offset..self.offset];
233 v.push(MacroValue::Text(Token::from_static_str(s, loc)));
234 self.value.push(LocaValue::Macro(v));
235 }
236
237 fn parse_keyword(&mut self) -> Option<MacroValue> {
238 self.next_char(); let loc = self.loc;
240 let start_offset = self.offset;
241 let key = self.get_key();
242 let end_offset = self.offset;
243 self.parse_format();
244 if self.chars.peek() != Some(&'$') {
245 let msg = "didn't recognize a key between $";
247 warn(ErrorKey::Localization).weak().msg(msg).loc(key).push();
248 return None;
249 }
250 self.next_char();
251 let s = &self.content[start_offset..end_offset];
252 Some(MacroValue::Keyword(Token::from_static_str(s, loc)))
253 }
254
255 fn get_rest_of_line(&mut self) -> &str {
256 let start_offset = self.offset;
257 while let Some(&c) = self.chars.peek() {
258 if c == '\n' {
259 break;
260 }
261 self.next_char();
262 }
263 let end_offset = self.offset;
264 self.next_char(); &self.content[start_offset..end_offset]
266 }
267
268 fn skip_until_key(&mut self) {
269 loop {
270 self.skip_whitespace();
272 if self.chars.peek() == Some(&'#') {
273 self.next_char();
274 if let Some(spec) = parse_comment(self.get_rest_of_line()) {
275 match spec.size {
276 IgnoreSize::Line => self.pending_line_ignores.push(spec.filter),
277 IgnoreSize::Block => (),
278 IgnoreSize::File => {
279 register_ignore_filter(self.loc.pathname(), .., spec.filter);
280 }
281 IgnoreSize::Begin => {
282 self.active_range_ignores.push((self.loc.line + 1, spec.filter));
283 }
284 IgnoreSize::End => {
285 if let Some((start_line, filter)) = self.active_range_ignores.pop() {
286 let path = self.loc.pathname();
287 register_ignore_filter(path, start_line..self.loc.line, filter);
288 }
289 }
290 }
291 }
292 continue;
293 }
294
295 match self.chars.peek() {
296 Some(&c) if is_key_char(c) => break,
297 Some(_) => {
298 self.unexpected_char("expected localization key");
299 self.skip_line();
300 }
301 None => break,
302 }
303 }
304 }
305
306 fn parse_loca(&mut self) -> Option<LocaEntry> {
308 self.skip_until_key();
311 self.chars.peek()?;
312 for filter in self.pending_line_ignores.drain(..) {
313 let path = self.loc.pathname();
314 let line = self.loc.line;
315 register_ignore_filter(path, line..=line, filter);
316 }
317
318 let key = self.get_key();
319 self.skip_linear_whitespace();
320 if self.chars.peek() == Some(&':') {
321 self.next_char();
322 } else {
323 self.unexpected_char("expected `:`");
324 return self.error_line(key);
325 }
326
327 while let Some(c) = self.chars.peek() {
329 if c.is_ascii_digit() {
330 self.next_char();
331 } else {
332 break;
333 }
334 }
335 self.skip_linear_whitespace();
336
337 if matches!(self.chars.peek(), Some('#' | '\n') | None) {
339 if self.expecting_language {
340 if !key.is(&format!("l_{}", self.language)) {
341 let msg = format!("wrong language header, should be `l_{}:`", self.language);
342 warn(ErrorKey::Localization).msg(msg).loc(key).push();
343 }
344 self.expecting_language = false;
345 self.skip_line();
346 return self.parse_loca();
348 }
349 warn(ErrorKey::Localization).msg("key with no value").loc(&key).push();
350 return self.error_line(key);
351 } else if self.expecting_language {
352 let msg = format!("expected language header `l_{}:`", self.language);
353 warn(ErrorKey::Localization).msg(msg).loc(&key).push();
354 self.expecting_language = false;
355 }
357 if self.chars.peek() == Some(&'"') {
358 self.next_char();
359 } else {
360 self.unexpected_char("expected `\"`");
361 return self.error_line(key);
362 }
363
364 if let Some(i) = self.find_dquote() {
369 self.loca_end = i;
370 } else {
371 let msg = "localization entry without ending quote";
372 warn(ErrorKey::Localization).msg(msg).loc(self.loc).push();
373 return self.error_line(key);
374 }
375
376 self.value = Vec::new();
377 let s = &self.content[self.offset..self.loca_end];
378 let token = Token::from_static_str(s, self.loc);
379
380 if self.line_has_macros() {
385 self.parse_macros();
386 if matches!(self.value.last(), Some(&LocaValue::Error)) {
387 return self.error_line(key);
388 }
389 } else {
390 self.value = ValueParser::new(vec![&token]).parse_vec();
391 while self.offset <= self.loca_end {
392 self.next_char();
393 }
394 }
395
396 self.skip_linear_whitespace();
397 match self.chars.peek() {
398 None | Some('#' | '\n') => (),
399 _ => {
400 let msg = "content after final `\"` on line";
401 warn(ErrorKey::Localization).strong().msg(msg).loc(self.loc).push();
402 }
403 }
404
405 self.skip_line();
406 let value = if self.value.len() == 1 {
407 self.value.remove(0)
408 } else {
409 LocaValue::Concat(take(&mut self.value))
410 };
411 Some(LocaEntry::new(key, value, Some(token)))
412 }
413}
414
415pub struct ValueParser<'a> {
416 loc: Loc,
417 offset: usize,
418 content: Vec<&'a Token>,
419 content_iters: Vec<Peekable<Chars<'a>>>,
420 content_idx: usize,
421 value: Vec<LocaValue>,
422}
423
424impl<'a> ValueParser<'a> {
426 pub fn new(content: Vec<&'a Token>) -> Self {
427 assert!(!content.is_empty());
428
429 Self {
430 loc: content[0].loc,
431 offset: 0,
432 content_iters: content.iter().map(|t| t.as_str().chars().peekable()).collect(),
433 content,
434 content_idx: 0,
435 value: Vec::new(),
436 }
437 }
438
439 fn maybe_advance_idx(&mut self) -> bool {
440 if self.content_idx + 1 == self.content.len() {
441 false
442 } else {
443 self.content_idx += 1;
444 self.loc = self.content[self.content_idx].loc;
445 self.offset = 0;
446 true
447 }
448 }
449
450 fn peek(&mut self) -> Option<char> {
451 if let Some(p) = self.content_iters[self.content_idx].peek() {
452 Some(*p)
453 } else if self.maybe_advance_idx() {
454 self.peek()
455 } else {
456 None
457 }
458 }
459
460 fn next_char(&mut self) {
461 if let Some(c) = self.content_iters[self.content_idx].next() {
462 self.offset += c.len_utf8();
463 self.loc.column += 1;
464 } else if self.maybe_advance_idx() {
465 self.next_char();
466 }
467 }
468
469 fn start_text(&self) -> Cob {
470 let mut cob = Cob::new();
471 cob.set(self.content[self.content_idx].as_str(), self.offset, self.loc);
472 cob
473 }
474
475 fn skip_whitespace(&mut self) {
476 while let Some(c) = self.peek() {
477 if c.is_whitespace() {
478 self.next_char();
479 } else {
480 break;
481 }
482 }
483 }
484
485 fn unexpected_char(&mut self, expected: &str, errorkey: ErrorKey) {
486 let c = self.peek().unwrap_or(' ');
488 let msg = format!("Unexpected character `{c}`, {expected}");
489 warn(errorkey).msg(msg).loc(self.loc).push();
490 }
491
492 fn get_key(&mut self) -> Token {
493 let mut text = self.start_text();
494 while let Some(c) = self.peek() {
495 if is_key_char(c) {
496 text.add_char(c);
497 self.next_char();
498 } else {
499 break;
500 }
501 }
502 text.take_to_token()
503 }
504
505 fn parse_format(&mut self) -> Option<Token> {
506 (self.peek() == Some('|')).then(|| {
507 self.next_char(); let mut text = self.start_text();
509 while let Some(c) = self.peek() {
510 if c == '$' || c == ']' {
511 break;
512 }
513 text.add_char(c);
514 self.next_char();
515 }
516 text.take_to_token()
517 })
518 }
519
520 fn parse_code_args(&mut self) -> Vec<CodeArg> {
521 self.next_char(); let mut v = Vec::new();
523
524 loop {
525 self.skip_whitespace();
526 if self.peek() == Some('\'') {
527 self.next_char();
528 let loc = self.loc;
529 let mut parens: isize = 0;
530 let mut text = self.start_text();
531 while let Some(c) = self.peek() {
532 match c {
533 '\'' => break,
534 ']' => {
535 let msg = "possible unterminated argument string";
536 let info = "Using [ ] inside argument strings does not work";
537 warn(ErrorKey::Localization).msg(msg).info(info).loc(self.loc).push();
538 }
539 ')' if parens == 0 => warn(ErrorKey::Localization)
540 .msg("possible unterminated argument string")
541 .loc(self.loc)
542 .push(),
543 '(' => parens += 1,
544 ')' => parens -= 1,
545 '\u{feff}' => {
546 let msg = "found unicode BOM in middle of file";
547 warn(ErrorKey::ParseError).strong().msg(msg).loc(loc).push();
548 }
549 _ => (),
550 }
551 text.add_char(c);
552 self.next_char();
553 }
554 if self.peek() != Some('\'') {
555 self.value.push(LocaValue::Error);
556 return Vec::new();
557 }
558 v.push(CodeArg::Literal(text.take_to_token()));
559 self.next_char();
560 } else if self.peek() == Some(')') {
561 } else {
563 v.push(CodeArg::Chain(self.parse_code_inner()));
564 }
565 self.skip_whitespace();
566 if self.peek() != Some(',') {
567 break;
568 }
569 self.next_char(); }
571 if self.peek() == Some(')') {
572 self.next_char();
573 } else {
574 self.unexpected_char("expected `)`", ErrorKey::Datafunctions);
575 }
576 v
577 }
578
579 fn parse_code_code(&mut self) -> Code {
580 let mut text = self.start_text();
581
582 if Game::is_hoi4() && self.peek() == Some('?') {
583 text.add_char('?');
584 self.next_char();
585 }
586
587 while let Some(c) = self.peek() {
588 if is_code_char(c) {
589 text.add_char(c);
590 self.next_char();
591 } else {
592 break;
593 }
594 }
595 let name = text.take_to_token();
596 if self.peek() == Some('(') {
597 Code { name, arguments: self.parse_code_args() }
598 } else {
599 Code { name, arguments: Vec::new() }
600 }
601 }
602
603 fn parse_code_inner(&mut self) -> CodeChain {
604 let mut v = Vec::new();
605 loop {
606 v.push(self.parse_code_code());
607 if matches!(self.peek(), Some('\r' | '\n')) {
610 self.next_char();
611 self.skip_whitespace();
612 }
613 if self.peek() != Some('.') {
614 break;
615 }
616 self.next_char(); }
618 CodeChain { codes: v.into_boxed_slice() }
619 }
620
621 fn parse_code(&mut self) {
622 self.next_char(); self.skip_whitespace();
624
625 let chain = self.parse_code_inner();
626
627 self.skip_whitespace();
628
629 let mut warned_extra_parens = false;
632 while self.peek() == Some(')') {
633 if !warned_extra_parens {
634 let msg = "too many `)`";
635 untidy(ErrorKey::Datafunctions).msg(msg).loc(self.loc).push();
636 warned_extra_parens = true;
637 }
638 self.next_char();
639 self.skip_whitespace();
640 }
641
642 let format = self.parse_format();
643
644 if self.peek() == Some(']') {
645 self.next_char();
646 self.value.push(LocaValue::Code(chain, format));
647 } else {
648 self.unexpected_char("expected `]`", ErrorKey::Datafunctions);
649 self.value.push(LocaValue::Error);
650 }
651 }
652
653 fn handle_tooltip(&mut self, value: &str, loc: Loc) {
654 if value.contains(',') {
655 let value = Token::new(value, loc);
658 let values: Vec<_> = value.split(',');
659 self.value
660 .push(LocaValue::ComplexTooltip(Box::new(values[0].clone()), values[1].clone()));
661 return;
662 }
663
664 self.value.push(LocaValue::Tooltip(Token::new(value, loc)));
666 }
667
668 fn parse_markup(&mut self) {
669 let loc = self.loc;
670 self.next_char(); if self.peek() == Some('#') {
672 self.next_char();
674 self.value.push(LocaValue::Text(Token::from_static_str("#", loc)));
675 } else if self.peek() == Some('!') {
676 self.next_char();
677 self.value.push(LocaValue::MarkupEnd);
678 } else {
679 enum State {
687 InKey(String),
688 InValue(String, String, Loc, usize),
689 }
690 let mut state = State::InKey(String::new());
691 while let Some(c) = self.peek() {
692 if c.is_whitespace() {
693 break;
694 }
695 let mut consumed = false;
696 match &mut state {
697 State::InKey(s) => {
698 if c == ':' {
699 if s.is_empty() {
700 self.unexpected_char("expected markup key", ErrorKey::Markup);
701 }
702 state = State::InValue(s.clone(), String::new(), self.loc, 0);
703 } else if c == ';' {
704 if s.is_empty() {
705 self.unexpected_char("expected markup key", ErrorKey::Markup);
706 }
707 state = State::InKey(String::new());
709 } else if c.is_alphanumeric() || c == '_' {
710 s.push(c);
711 } else {
712 break;
713 }
714 }
715 State::InValue(key, value, loc, bracecount) => {
716 if c == ':' {
717 value.push(c);
718 self.unexpected_char("expected `;`", ErrorKey::Markup);
719 } else if c == ';' {
720 if key.eq_ignore_ascii_case("tooltip") {
721 self.handle_tooltip(value, *loc);
722 }
723 state = State::InKey(String::new());
724 } else if c == '{' {
725 *bracecount += 1;
726 } else if c == '}' {
727 if *bracecount > 0 {
728 *bracecount -= 1;
729 } else {
730 let msg = "mismatched braces in markup";
731 warn(ErrorKey::Markup).msg(msg).loc(self.loc).push();
732 self.value.push(LocaValue::Error);
733 }
734 } else if c == '.'
735 || c == ','
736 || c.is_alphanumeric()
737 || c == '_'
738 || c == '|'
739 {
740 value.push(c);
744 } else if c == '[' {
745 self.parse_code();
749 consumed = true;
750 } else {
751 break;
752 }
753 }
754 }
755 if !consumed {
756 self.next_char();
757 }
758 }
759 match state {
761 State::InKey(_) => {
762 self.value.push(LocaValue::Markup);
763 }
764 State::InValue(key, value, loc, bracecount) => {
765 if key.eq_ignore_ascii_case("tooltip") {
766 self.handle_tooltip(&value, loc);
767 }
768 if bracecount > 0 {
769 let msg = "mismatched braces in markup";
770 warn(ErrorKey::Markup).msg(msg).loc(self.loc).push();
771 self.value.push(LocaValue::Error);
772 } else {
773 self.value.push(LocaValue::Markup);
774 }
775 }
776 }
777 if self.peek().is_none_or(char::is_whitespace) {
778 self.next_char();
779 } else {
780 let msg = "#markup should be followed by a space";
781 warn(ErrorKey::Markup).msg(msg).loc(self.loc).push();
782 self.value.push(LocaValue::Error);
783 }
784 }
785 }
786
787 fn parse_icon(&mut self) {
788 self.next_char(); let mut old_value = take(&mut self.value);
791
792 while let Some(c) = self.peek() {
793 if c == '[' {
794 self.parse_code();
795 } else if is_key_char(c) {
796 let key = self.get_key();
797 self.value.push(LocaValue::Text(key));
798 } else if c == '!' {
799 self.next_char();
800 break;
801 } else if self.value.is_empty() {
802 self.unexpected_char("expected icon name", ErrorKey::Localization);
803 self.value.push(LocaValue::Error);
804 break;
805 } else {
806 self.unexpected_char("expected `!`", ErrorKey::Localization);
807 self.value.push(LocaValue::Error);
808 break;
809 }
810 }
811
812 if matches!(self.value.last(), Some(LocaValue::Error)) {
813 old_value.push(LocaValue::Error);
814 self.value = take(&mut old_value);
815 } else if self.value.len() == 1 {
816 if let Some(LocaValue::Text(icon)) = self.value.last() {
817 old_value.push(LocaValue::Icon(icon.clone()));
819 self.value = take(&mut old_value);
820 } else {
821 old_value.push(LocaValue::CalculatedIcon(take(&mut self.value)));
823 self.value = take(&mut old_value);
824 }
825 } else {
826 old_value.push(LocaValue::CalculatedIcon(take(&mut self.value)));
827 self.value = take(&mut old_value);
828 }
829 }
830
831 #[allow(dead_code)] fn parse_flag(&mut self) {
833 self.next_char(); let mut text = self.start_text();
836 while let Some(c) = self.peek() {
837 if c.is_ascii_uppercase() {
838 text.add_char(c);
839 self.next_char();
840 } else {
841 break;
842 }
843 }
844 let flag = text.take_to_token();
845 if flag.is("") {
846 self.unexpected_char("expected country tag", ErrorKey::Localization);
847 self.value.push(LocaValue::Error);
848 } else {
849 self.value.push(LocaValue::Flag(flag));
850 }
851 }
852
853 fn parse_escape(&mut self) {
854 let loc = self.loc;
855 self.next_char(); let s = match self.peek() {
857 Some('n') => '\n'.to_string(),
858 Some(c) => c.to_string(),
859 None => {
860 self.value.push(LocaValue::Error);
861 return;
862 }
863 };
864 self.next_char();
865 self.value.push(LocaValue::Text(Token::new(&s, loc)));
866 }
867
868 fn parse_text(&mut self) {
869 let mut text = self.start_text();
870 while let Some(c) = self.peek() {
871 match c {
872 '[' | '#' | '@' | '\\' => break,
873 _ => {
874 text.add_char(c);
875 self.next_char();
876 }
877 }
878 }
879 self.value.push(LocaValue::Text(text.take_to_token()));
880 }
881
882 pub fn parse_vec(mut self) -> Vec<LocaValue> {
883 while let Some(c) = self.peek() {
884 match c {
885 '[' => self.parse_code(),
886 '#' => self.parse_markup(),
887 '@' if Game::is_hoi4() => self.parse_flag(),
888 '@' => self.parse_icon(),
889 '\\' => self.parse_escape(),
890 _ => self.parse_text(),
891 }
892 if matches!(self.value.last(), Some(&LocaValue::Error)) {
893 return vec![LocaValue::Error];
894 }
895 }
896 self.value
897 }
898
899 pub fn parse(self) -> LocaValue {
900 let mut value = self.parse_vec();
901 if value.len() == 1 { value.remove(0) } else { LocaValue::Concat(value) }
902 }
903}
904
905pub struct LocaReader {
906 parser: LocaParser,
907}
908
909impl Iterator for LocaReader {
910 type Item = LocaEntry;
911
912 fn next(&mut self) -> Option<Self::Item> {
913 self.parser.parse_loca()
914 }
915}
916
917pub fn parse_loca(entry: &FileEntry, content: String, lang: Language) -> LocaReader {
918 let content = content.leak();
919 let parser = LocaParser::new(entry, content, lang);
920 LocaReader { parser }
921}