tiger_lib/parse/
localization.rs

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
19// code_char might end up being identical to key_char, since we can write [gameconcept] and
20// game concepts can be any id
21fn 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.line == 0 making this a whole file report
46        loc.column = 1; // From our perspective the BOM is a character and needs to be included in column offset
47
48        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                // Second BOM is file content, not header, and should be reported with line number
55                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        // From here on we are reporting on file content
68        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        // self.loc is always the loc of the peekable char
86        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(); // Eat the newline
125    }
126
127    // This function returns an Option so that the caller can return
128    // its value without further boilerplate.
129    #[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    // Look ahead to the last `"` on the line
163    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(); // eat the |
180            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        // TODO: vanilla uses $[DATE_MIN.GetStringShort|V]$ which breaks all my assumptions
206        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(); // Skip the $
239        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            // TODO: check if there is a closing $, adapt warning text
246            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(); // Eat the newline
265        &self.content[start_offset..end_offset]
266    }
267
268    fn skip_until_key(&mut self) {
269        loop {
270            // Skip comments and blank lines
271            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    /// Return the next `LocaEntry`, or None if there are no more in the file.
307    fn parse_loca(&mut self) -> Option<LocaEntry> {
308        // Loop until we have a key. Once we have a key, we'll definitely
309        // return a LocaEntry for the current line, though it might be an Error entry.
310        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        // Skip optional number after :
328        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        // Now we should see the value. But what if the line ends here?
338        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                // Recursing here is safe because it can happen only once.
347                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            // Continue to parse this entry as usual
356        }
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        // We need to pre-parse because the termination of localization entries
365        // is ambiguous. A loca value ends at the last " on the line.
366        // Any # or " before that are part of the value; an # after that
367        // introduces a comment.
368        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        // We also need to pre-parse because $macros$ can appear anywhere and
381        // we don't know how to parse the results until we know what to
382        // substitute. If there are macros in the line, return it as a special
383        // `LocaValue::Macro` array
384        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
424// TODO: some duplication of helper functions between `LocaParser` and `ValueParser`
425impl<'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        // TODO: handle EOF better
487        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(); // eat the |
508            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(); // eat the opening (
522        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                // Empty () means no arguments
562            } 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(); // Eat the comma
570        }
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            // Newlines followed by whitespace are allowed in code sequences,
608            // but not random whitespace.
609            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(); // Eat the '.'
617        }
618        CodeChain { codes: v.into_boxed_slice() }
619    }
620
621    fn parse_code(&mut self) {
622        self.next_char(); // eat the opening [
623        self.skip_whitespace();
624
625        let chain = self.parse_code_inner();
626
627        self.skip_whitespace();
628
629        // The game engine doesn't mind if there are too many `)` before the `]`, so handle
630        // that at "untidy" severity.
631        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            // If the value contains commas, then it's #tooltip:tag,key or #tooltip:tag,key,value
656            // Separate out the tooltip.
657            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        // Otherwise, then it's just #tooltip:tooltip
665        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(); // skip the #
671        if self.peek() == Some('#') {
672            // double # means a literal #
673            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            // examples:
680            // #indent_newline:2
681            // #color:{1.0,1.0,1.0}
682            // #font:TitleFont
683            // #tooltippable;positive_value;TOOLTIP:expedition_progress_explanation_tt
684            // #TOOLTIP:GAME_TRAIT,lifestyle_physician,[GetNullCharacter]
685            // #tooltip:[Party.GetTooltipTag]|[InterestGroup.GetTooltipTag],INTEREST_GROUP_AFFILIATION_BREAKDOWN
686            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                            // TODO: warn about markup keys that expect a value
708                            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                            // . and , are freely allowed in markup values because with #tooltip
741                            // the value might be a loca key.
742                            // | is allowed because in some tooltips it separates components of the tooltip tag
743                            value.push(c);
744                        } else if c == '[' {
745                            // Generating part of the markup with a code block is valid.
746                            // Assume (hope) that it generates the current value and not some random chunk
747                            // of markup. The next thing we see ought to be a comma or a space.
748                            self.parse_code();
749                            consumed = true;
750                        } else {
751                            break;
752                        }
753                    }
754                }
755                if !consumed {
756                    self.next_char();
757                }
758            }
759            // Clean up leftover state at end
760            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(); // eat the @
789
790        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                // The usual case: a simple @icon!
818                old_value.push(LocaValue::Icon(icon.clone()));
819                self.value = take(&mut old_value);
820            } else {
821                // This can happen if the whole icon is a @[...]!
822                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)] // only needed for hoi4
832    fn parse_flag(&mut self) {
833        self.next_char(); // eat the @
834
835        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(); // Skip the \
856        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}