msg_tool\scripts\artemis/
asb.rs

1//! Artemis Engine ASB file (.asb/.iet)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::*;
6use crate::utils::escape::*;
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, HashSet};
10use std::io::{Read, Write};
11use std::ops::Index;
12use std::sync::Arc;
13use stylua_lib::{Config as LuaFormatterConfig, OutputVerification, format_code};
14use unicode_segmentation::UnicodeSegmentation;
15
16#[derive(Debug)]
17/// The builder for Artemis ASB scripts.
18pub struct ArtemisAsbBuilder {}
19
20impl ArtemisAsbBuilder {
21    /// Creates a new instance of `ArtemisAsbBuilder`.
22    pub fn new() -> Self {
23        ArtemisAsbBuilder {}
24    }
25}
26
27impl ScriptBuilder for ArtemisAsbBuilder {
28    fn default_encoding(&self) -> Encoding {
29        Encoding::Utf8
30    }
31
32    fn build_script(
33        &self,
34        buf: Vec<u8>,
35        filename: &str,
36        encoding: Encoding,
37        _archive_encoding: Encoding,
38        config: &ExtraConfig,
39        _archive: Option<&Box<dyn Script>>,
40    ) -> Result<Box<dyn Script + Send + Sync>> {
41        Ok(Box::new(Asb::new(buf, encoding, config, filename)?))
42    }
43
44    fn extensions(&self) -> &'static [&'static str] {
45        &["asb", "iet"]
46    }
47
48    fn script_type(&self) -> &'static ScriptType {
49        &ScriptType::ArtemisAsb
50    }
51
52    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
53        if buf_len >= 5 && buf.starts_with(b"ASB\0\0") {
54            return Some(20);
55        }
56        None
57    }
58
59    fn can_create_file(&self) -> bool {
60        true
61    }
62
63    fn create_file<'a>(
64        &'a self,
65        filename: &'a str,
66        writer: Box<dyn WriteSeek + 'a>,
67        encoding: Encoding,
68        file_encoding: Encoding,
69        config: &ExtraConfig,
70    ) -> Result<()> {
71        create_file(
72            filename,
73            writer,
74            encoding,
75            file_encoding,
76            config.custom_yaml,
77        )
78    }
79}
80
81fn escape_text(s: &str) -> String {
82    let mut escaped = String::with_capacity(s.len());
83    for c in s.chars() {
84        match c {
85            '&' => escaped.push_str("&amp;"),
86            '<' => escaped.push_str("&lt;"),
87            _ => escaped.push(c),
88        }
89    }
90    escaped
91}
92
93#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
94struct Command {
95    pub name: String,
96    pub line_number: u32,
97    pub attributes: BTreeMap<String, String>,
98}
99
100impl Command {
101    pub fn new(name: String, line_number: u32) -> Self {
102        Command {
103            name,
104            line_number,
105            attributes: BTreeMap::new(),
106        }
107    }
108
109    pub fn to_xml(&self) -> String {
110        let mut xml = format!("<{}", self.name);
111        for (key, value) in &self.attributes {
112            xml.push_str(&format!(" {}=\"{}\"", key, escape_xml_text_value(value)));
113        }
114        xml.push('>');
115        xml
116    }
117}
118
119impl<'a> Index<&'a str> for Command {
120    type Output = str;
121    fn index(&self, key: &'a str) -> &Self::Output {
122        self.attributes.get(key).map_or("", |s| s.as_str())
123    }
124}
125
126impl<'a> Index<&'a String> for Command {
127    type Output = str;
128    fn index(&self, key: &'a String) -> &Self::Output {
129        self.attributes.get(key).map_or("", |s| s.as_str())
130    }
131}
132
133impl Index<String> for Command {
134    type Output = str;
135    fn index(&self, key: String) -> &Self::Output {
136        self.attributes.get(&key).map_or("", |s| s.as_str())
137    }
138}
139
140#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
141#[serde(untagged)]
142enum Item {
143    Command(Command),
144    Label(String),
145}
146
147impl Item {
148    pub fn is_command(&self) -> bool {
149        matches!(self, Item::Command(_))
150    }
151
152    pub fn command_name_in(&self, names: &HashSet<String>) -> Option<String> {
153        if let Item::Command(cmd) = self {
154            if names.contains(&cmd.name) {
155                return Some(cmd.name.clone());
156            }
157        }
158        None
159    }
160}
161
162trait CustomReadFn {
163    fn read_string(&mut self, encoding: Encoding) -> Result<String>;
164    fn read_item(&mut self, encoding: Encoding) -> Result<Item>;
165}
166
167impl<T: Read> CustomReadFn for T {
168    fn read_string(&mut self, encoding: Encoding) -> Result<String> {
169        let len = self.read_u32()?;
170        let data = self.read_exact_vec(len as usize)?;
171        if self.read_u8()? != 0 {
172            return Err(anyhow::anyhow!("String not null-terminated"));
173        }
174        let s = decode_to_string(encoding, &data, true)?;
175        Ok(s)
176    }
177
178    fn read_item(&mut self, encoding: Encoding) -> Result<Item> {
179        let typ = self.read_u32()?;
180        match typ {
181            0 => {
182                let name = self.read_string(encoding)?;
183                let line_number = self.read_u32()?;
184                let mut command = Command::new(name, line_number);
185                let attr_count = self.read_u32()?;
186                for _ in 0..attr_count {
187                    let key = self.read_string(encoding)?;
188                    let value = self.read_string(encoding)?;
189                    command.attributes.insert(key, value);
190                }
191                Ok(Item::Command(command))
192            }
193            1 => {
194                let label = self.read_string(encoding)?;
195                Ok(Item::Label(label))
196            }
197            _ => {
198                return Err(anyhow::anyhow!("Unknown item type: {}", typ));
199            }
200        }
201    }
202}
203
204trait CustomWriteFn {
205    fn write_string(&mut self, s: &str, encoding: Encoding) -> Result<()>;
206    fn write_item(&mut self, item: &Item, encoding: Encoding) -> Result<()>;
207}
208
209impl<T: Write> CustomWriteFn for T {
210    fn write_string(&mut self, s: &str, encoding: Encoding) -> Result<()> {
211        let data = encode_string(encoding, s, false)?;
212        self.write_u32(data.len() as u32)?;
213        self.write_all(&data)?;
214        self.write_u8(0)?; // Null-terminated
215        Ok(())
216    }
217
218    fn write_item(&mut self, item: &Item, encoding: Encoding) -> Result<()> {
219        match item {
220            Item::Command(cmd) => {
221                self.write_u32(0)?; // Type 0 for Command
222                self.write_string(&cmd.name, encoding)?;
223                self.write_u32(cmd.line_number)?;
224                self.write_u32(cmd.attributes.len() as u32)?;
225                for (key, value) in &cmd.attributes {
226                    self.write_string(key, encoding)?;
227                    self.write_string(value, encoding)?;
228                }
229            }
230            Item::Label(label) => {
231                self.write_u32(1)?; // Type 1 for Label
232                self.write_string(label, encoding)?;
233            }
234        }
235        Ok(())
236    }
237}
238
239struct TextParser<'a> {
240    items: Vec<Item>,
241    text: Vec<&'a str>,
242    pos: usize,
243    len: usize,
244    hcls_index: usize,
245    end_tag: &'a str,
246}
247
248impl<'a> TextParser<'a> {
249    pub fn new(str: &'a str, hcls_index: usize, end_tag: &'a str) -> Self {
250        let text: Vec<&'a str> = UnicodeSegmentation::graphemes(str, true).collect();
251        let len = text.len();
252        TextParser {
253            items: Vec::new(),
254            text,
255            pos: 0,
256            len,
257            hcls_index,
258            end_tag,
259        }
260    }
261
262    pub fn parse(mut self) -> Result<Vec<Item>> {
263        while let Some(c) = self.peek() {
264            match c {
265                "<" => {
266                    self.parse_tag()?;
267                }
268                _ => {
269                    let mut text = String::new();
270                    self.eat_char();
271                    text.push_str(c);
272                    while let Some(b) = self.peek() {
273                        if b == "<" {
274                            break;
275                        }
276                        text.push_str(b);
277                        self.eat_char();
278                    }
279                    if !text.is_empty() {
280                        self.items.push(Item::Command(Command {
281                            name: "print".to_string(),
282                            line_number: 0,
283                            attributes: [("data".to_string(), unescape_xml(&text))].into(),
284                        }))
285                    }
286                }
287            }
288        }
289        let mut hcls = Command::new(self.end_tag.to_string(), 0);
290        if self.end_tag == "hcls" {
291            hcls.attributes
292                .insert("0".to_string(), self.hcls_index.to_string());
293        }
294        self.items.push(Item::Command(hcls));
295        Ok(self.items)
296    }
297
298    fn parse_tag(&mut self) -> Result<()> {
299        self.parse_indent("<")?;
300        let key = self.parse_key()?;
301        self.erase_whitespace();
302        let mut cmd = Command::new(key, 0);
303        loop {
304            let c = self.peek().ok_or(self.error2("Unexpected eof"))?;
305            match c {
306                ">" => {
307                    self.eat_char();
308                    break;
309                }
310                " " => {
311                    self.eat_char();
312                    continue;
313                }
314                _ => {
315                    let key = self.parse_key()?;
316                    self.parse_indent("=")?;
317                    let value = self.parse_str()?;
318                    cmd.attributes.insert(key, value);
319                }
320            }
321        }
322        if self.items.is_empty() {
323            self.items
324                .push(Item::Command(Command::new("msgon".to_string(), 0)));
325        }
326        self.items.push(Item::Command(cmd));
327        Ok(())
328    }
329
330    fn parse_key(&mut self) -> Result<String> {
331        self.erase_whitespace();
332        let mut key = String::new();
333        while let Some(c) = self.peek() {
334            if c == "=" || c == " " || c == ">" {
335                break;
336            }
337            key.push_str(c);
338            self.eat_char();
339        }
340        if key.is_empty() {
341            return self.error("Expected key, but found nothing");
342        }
343        Ok(key)
344    }
345
346    fn parse_str(&mut self) -> Result<String> {
347        self.erase_whitespace();
348        self.parse_indent("\"")?;
349        let mut text = String::new();
350        loop {
351            match self.next().ok_or(self.error2("Unexpected eof"))? {
352                "\"" => {
353                    break;
354                }
355                t => {
356                    text.push_str(t);
357                }
358            }
359        }
360        Ok(unescape_xml(&text))
361    }
362
363    fn erase_whitespace(&mut self) {
364        while let Some(c) = self.peek() {
365            if c == " " {
366                self.eat_char();
367            } else {
368                break;
369            }
370        }
371    }
372
373    fn parse_indent(&mut self, indent: &str) -> Result<()> {
374        for ident in indent.graphemes(true) {
375            match self.next() {
376                Some(c) => {
377                    if c != ident {
378                        return self.error("Unexpected indent");
379                    }
380                }
381                None => return self.error("Unexpected eof"),
382            }
383        }
384        Ok(())
385    }
386
387    fn eat_char(&mut self) {
388        if self.pos < self.len {
389            self.pos += 1;
390        }
391    }
392
393    fn next(&mut self) -> Option<&'a str> {
394        if self.pos < self.len {
395            let item = self.text[self.pos];
396            self.pos += 1;
397            Some(item)
398        } else {
399            None
400        }
401    }
402
403    fn peek(&self) -> Option<&'a str> {
404        if self.pos < self.len {
405            Some(self.text[self.pos])
406        } else {
407            None
408        }
409    }
410
411    fn error2<T>(&self, msg: T) -> anyhow::Error
412    where
413        T: std::fmt::Display,
414    {
415        anyhow::anyhow!("Failed to parse at position {}: {}", self.pos, msg)
416    }
417
418    fn error<T, A>(&self, msg: T) -> Result<A>
419    where
420        T: std::fmt::Display,
421    {
422        Err(anyhow::anyhow!(
423            "Failed to parse at position {}: {}",
424            self.pos,
425            msg
426        ))
427    }
428}
429
430#[derive(Debug)]
431/// The Artemis ASB script.
432pub struct Asb {
433    items: Vec<Item>,
434    custom_yaml: bool,
435    is_iet: bool,
436    format_lua: bool,
437    end_tags: Arc<HashSet<String>>,
438}
439
440impl Asb {
441    /// Creates a new Artemis ASB script from the given buffer.
442    ///
443    /// * `buf` - The buffer containing the ASB data.
444    /// * `encoding` - The encoding used for the ASB data.
445    /// * `config` - Extra configuration options.
446    pub fn new(
447        buf: Vec<u8>,
448        encoding: Encoding,
449        config: &ExtraConfig,
450        filename: &str,
451    ) -> Result<Self> {
452        let mut data = MemReader::new(buf);
453        let mut magic = [0; 5];
454        data.read_exact(&mut magic)?;
455        if &magic != b"ASB\0\0" {
456            return Err(anyhow::anyhow!("Invalid ASB magic number: {:?}", magic));
457        }
458        let nums = data.read_u32()?;
459        let mut items = Vec::with_capacity(nums as usize);
460        for _ in 0..nums {
461            items.push(data.read_item(encoding)?);
462        }
463        Ok(Asb {
464            items,
465            custom_yaml: config.custom_yaml,
466            is_iet: std::path::Path::new(filename)
467                .extension()
468                .map_or(false, |ext| ext.eq_ignore_ascii_case("iet")),
469            format_lua: config.artemis_asb_format_lua,
470            end_tags: config.artemis_asb_end_tags.clone(),
471        })
472    }
473
474    fn to_string(&self, items: &[Item]) -> Result<String> {
475        if self.custom_yaml {
476            Ok(serde_yaml_ng::to_string(items)?)
477        } else {
478            Ok(serde_json::to_string_pretty(items)?)
479        }
480    }
481
482    fn format_lua(&self, script: &str) -> Result<String> {
483        let mut config = LuaFormatterConfig::new();
484        config.indent_type = stylua_lib::IndentType::Spaces;
485        config.indent_width = 2;
486        config.column_width = 120;
487        config.line_endings = stylua_lib::LineEndings::Unix;
488        Ok(format_code(script, config, None, OutputVerification::None)?)
489    }
490}
491
492impl Script for Asb {
493    fn default_output_script_type(&self) -> OutputScriptType {
494        if self.is_iet {
495            OutputScriptType::Custom
496        } else {
497            OutputScriptType::Json
498        }
499    }
500
501    fn is_output_supported(&self, out: OutputScriptType) -> bool {
502        if self.is_iet {
503            matches!(out, OutputScriptType::Custom)
504        } else {
505            true
506        }
507    }
508
509    fn default_format_type(&self) -> FormatOptions {
510        FormatOptions::None
511    }
512
513    fn custom_output_extension<'a>(&'a self) -> &'a str {
514        if self.custom_yaml { "yaml" } else { "json" }
515    }
516
517    fn extract_messages(&self) -> Result<Vec<Message>> {
518        let mut messages = Vec::new();
519        let mut name = None;
520        let mut cur_mes = String::new();
521        let mut in_print = false;
522        for item in self.items.iter() {
523            if in_print {
524                if let Item::Command(cmd) = item {
525                    match cmd.name.as_str() {
526                        "print" => {
527                            cur_mes.push_str(&escape_text(&cmd["data"]));
528                        }
529                        "rt" => {
530                            cur_mes.push('\n');
531                        }
532                        _ => {
533                            if self.end_tags.contains(&cmd.name) {
534                                in_print = false;
535                                messages.push(Message {
536                                    name: name.take(),
537                                    message: cur_mes,
538                                });
539                                cur_mes = String::new();
540                                continue;
541                            }
542                            cur_mes.push_str(&cmd.to_xml());
543                        }
544                    }
545                    continue;
546                }
547            }
548            if let Item::Command(cmd) = item {
549                match cmd.name.as_str() {
550                    "print" => {
551                        cur_mes.push_str(&escape_text(&cmd["data"]));
552                        in_print = true;
553                    }
554                    "name" => {
555                        let v = (cmd.attributes.len() - 1).to_string();
556                        name = Some(cmd[v].to_owned());
557                    }
558                    "msgon" => {
559                        in_print = true;
560                    }
561                    "sel_text" => {
562                        let t = &cmd["text"];
563                        if !t.is_empty() {
564                            messages.push(Message {
565                                name: None,
566                                message: t.to_owned(),
567                            });
568                        }
569                    }
570                    "RegisterTextToHistory" => {
571                        let t = &cmd["1"];
572                        if !t.is_empty() {
573                            messages.push(Message {
574                                name: None,
575                                message: t.to_owned(),
576                            });
577                        }
578                    }
579                    _ => {}
580                }
581            }
582        }
583        if !cur_mes.is_empty() {
584            messages.push(Message {
585                name: name.take(),
586                message: cur_mes,
587            });
588        }
589        Ok(messages)
590    }
591
592    fn import_messages<'a>(
593        &'a self,
594        messages: Vec<Message>,
595        mut file: Box<dyn WriteSeek + 'a>,
596        _filename: &str,
597        encoding: Encoding,
598        replacement: Option<&'a ReplacementTable>,
599    ) -> Result<()> {
600        file.write_all(b"ASB\0\0")?;
601        let mut items = self.items.clone();
602        let mut name_index = None;
603        let mut mes_index = 0;
604        let mut item_index = 0;
605        let mut print_index = None;
606        let mut hcls_index = 1;
607        while item_index < items.len() {
608            if let Some(print_ind) = print_index.clone() {
609                if let Some(end_tag) = items[item_index].command_name_in(&self.end_tags) {
610                    let message = messages
611                        .get(mes_index)
612                        .ok_or(anyhow::anyhow!("Not enough messages."))?;
613                    if let Some(name_index) = name_index.take() {
614                        let mut name = match &message.name {
615                            Some(name) => name.to_owned(),
616                            None => return Err(anyhow::anyhow!("Message without name.")),
617                        };
618                        if let Some(replacement) = replacement {
619                            for (k, v) in &replacement.map {
620                                name = name.replace(k, v);
621                            }
622                        }
623                        if let Item::Command(cmd) = &mut items[name_index] {
624                            if cmd.attributes.len() > 1 {
625                                cmd.attributes
626                                    .insert(format!("{}", cmd.attributes.len() - 1), name);
627                            } else {
628                                let oname = cmd
629                                    .attributes
630                                    .get("0")
631                                    .ok_or(anyhow::anyhow!("No name attribute found."))?;
632                                if oname != &name {
633                                    cmd.attributes.insert("1".to_string(), name);
634                                }
635                            }
636                        }
637                    }
638                    let mut m = message.message.clone();
639                    if let Some(replacement) = replacement {
640                        for (k, v) in &replacement.map {
641                            m = m.replace(k, v);
642                        }
643                    }
644                    let new_cmds =
645                        TextParser::new(&m.replace("\n", "<rt>"), hcls_index, &end_tag).parse()?;
646                    hcls_index += 1;
647                    let new_cmds_len = new_cmds.len();
648                    items.splice(print_ind..=item_index, new_cmds);
649                    print_index = None;
650                    item_index = print_ind + new_cmds_len;
651                    mes_index += 1;
652                    continue;
653                } else if items[item_index].is_command() {
654                    item_index += 1;
655                    continue;
656                }
657            }
658            if let Item::Command(cmd) = &mut items[item_index] {
659                match cmd.name.as_str() {
660                    "print" => {
661                        print_index = Some(item_index);
662                    }
663                    "msgon" => {
664                        print_index = Some(item_index);
665                    }
666                    "name" => {
667                        name_index = Some(item_index);
668                    }
669                    "sel_text" => {
670                        let message = messages
671                            .get(mes_index)
672                            .ok_or(anyhow::anyhow!("Not enough messages."))?;
673                        let mut m = message.message.clone();
674                        if let Some(replacement) = replacement {
675                            for (k, v) in &replacement.map {
676                                m = m.replace(k, v);
677                            }
678                        }
679                        cmd.attributes.insert("text".to_string(), m);
680                        mes_index += 1;
681                    }
682                    "RegisterTextToHistory" => {
683                        let message = messages
684                            .get(mes_index)
685                            .ok_or(anyhow::anyhow!("Not enough messages."))?;
686                        let mut m = message.message.clone();
687                        if let Some(replacement) = replacement {
688                            for (k, v) in &replacement.map {
689                                m = m.replace(k, v);
690                            }
691                        }
692                        cmd.attributes.insert("1".to_string(), m);
693                        mes_index += 1;
694                    }
695                    _ => {}
696                }
697            }
698            item_index += 1;
699        }
700        if mes_index != messages.len() {
701            return Err(anyhow::anyhow!(
702                "Not all messages were processed, expected {}, got {}",
703                messages.len(),
704                mes_index
705            ));
706        }
707        file.write_u32(items.len() as u32)?;
708        for item in items {
709            file.write_item(&item, encoding)?;
710        }
711        file.flush()?;
712        Ok(())
713    }
714
715    fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> {
716        let s = if self.format_lua {
717            let items: Vec<_> = self
718                .items
719                .iter()
720                .map(|s| {
721                    if let Item::Command(cmd) = s {
722                        if cmd.name == "lua" {
723                            if let Some(script) = cmd.attributes.get("script") {
724                                let mut cmd = cmd.clone();
725                                cmd.attributes.insert(
726                                    "script".to_string(),
727                                    match self.format_lua(script) {
728                                        Ok(s) => s,
729                                        Err(_) => {
730                                            eprintln!("Warning: Failed to format Lua script.");
731                                            crate::COUNTER.inc_warning();
732                                            script.clone()
733                                        }
734                                    },
735                                );
736                                return Item::Command(cmd);
737                            }
738                        }
739                    }
740                    s.clone()
741                })
742                .collect();
743            self.to_string(&items)?
744        } else {
745            self.to_string(&self.items)?
746        };
747        let s = encode_string(encoding, &s, false)?;
748        let mut file = std::fs::File::create(filename)?;
749        file.write_all(&s)?;
750        Ok(())
751    }
752
753    fn custom_import<'a>(
754        &'a self,
755        custom_filename: &'a str,
756        file: Box<dyn WriteSeek + 'a>,
757        encoding: Encoding,
758        output_encoding: Encoding,
759    ) -> Result<()> {
760        create_file(
761            custom_filename,
762            file,
763            encoding,
764            output_encoding,
765            self.custom_yaml,
766        )
767    }
768}
769
770/// Creates a new ASB file.
771///
772/// * `custom_filename` - The path ot the input file.
773/// * `writer` - The writer to write the ASB script.
774/// * `encoding` - The encoding used for the ASB script.
775/// * `output_encoding` - The encoding used for the input file.
776/// * `yaml` - Whether to use YAML format instead of JSON for the input file.
777pub fn create_file<'a>(
778    custom_filename: &'a str,
779    mut writer: Box<dyn WriteSeek + 'a>,
780    encoding: Encoding,
781    output_encoding: Encoding,
782    yaml: bool,
783) -> Result<()> {
784    let f = crate::utils::files::read_file(custom_filename)?;
785    let s = decode_to_string(output_encoding, &f, true)?;
786    let items: Vec<Item> = if yaml {
787        serde_yaml_ng::from_str(&s)?
788    } else {
789        serde_json::from_str(&s)?
790    };
791    writer.write_all(b"ASB\0\0")?;
792    writer.write_u32(items.len() as u32)?;
793    for item in items {
794        writer.write_item(&item, encoding)?;
795    }
796    Ok(())
797}
798
799#[test]
800fn test_parse() {
801    let text = "Hello &lt; &amp; World!<tag><tags x=\"123\"><name 0=\"Ok\">Test";
802    let parser = TextParser::new(text, 1, "hcls");
803    let items = parser.parse().unwrap();
804    assert_eq!(
805        items,
806        vec![
807            Item::Command(Command {
808                name: "print".to_string(),
809                line_number: 0,
810                attributes: [("data".to_string(), "Hello < & World!".to_string())].into(),
811            }),
812            Item::Command(Command {
813                name: "tag".to_string(),
814                line_number: 0,
815                attributes: BTreeMap::new(),
816            }),
817            Item::Command(Command {
818                name: "tags".to_string(),
819                line_number: 0,
820                attributes: [("x".to_string(), "123".to_string())].into(),
821            }),
822            Item::Command(Command {
823                name: "name".to_string(),
824                line_number: 0,
825                attributes: [("0".to_string(), "Ok".to_string())].into(),
826            }),
827            Item::Command(Command {
828                name: "print".to_string(),
829                line_number: 0,
830                attributes: [("data".to_string(), "Test".to_string())].into(),
831            }),
832            Item::Command(Command {
833                name: "hcls".to_string(),
834                line_number: 0,
835                attributes: BTreeMap::from([("0".to_string(), "1".to_string())]),
836            }),
837        ]
838    )
839}
840
841#[test]
842fn test_parse2() {
843    let text = "<ruby text=\"test\">OK</ruby>Hello &lt; &amp; World!<tag><tags x=\"123\"><name 0=\"Ok\">Test";
844    let parser = TextParser::new(text, 1, "click");
845    let items = parser.parse().unwrap();
846    assert_eq!(
847        items,
848        vec![
849            Item::Command(Command {
850                name: "msgon".to_string(),
851                line_number: 0,
852                attributes: BTreeMap::new(),
853            }),
854            Item::Command(Command {
855                name: "ruby".to_string(),
856                line_number: 0,
857                attributes: [("text".to_string(), "test".to_string())].into(),
858            }),
859            Item::Command(Command {
860                name: "print".to_string(),
861                line_number: 0,
862                attributes: [("data".to_string(), "OK".to_string())].into(),
863            }),
864            Item::Command(Command {
865                name: "/ruby".to_string(),
866                line_number: 0,
867                attributes: BTreeMap::new(),
868            }),
869            Item::Command(Command {
870                name: "print".to_string(),
871                line_number: 0,
872                attributes: [("data".to_string(), "Hello < & World!".to_string())].into(),
873            }),
874            Item::Command(Command {
875                name: "tag".to_string(),
876                line_number: 0,
877                attributes: BTreeMap::new(),
878            }),
879            Item::Command(Command {
880                name: "tags".to_string(),
881                line_number: 0,
882                attributes: [("x".to_string(), "123".to_string())].into(),
883            }),
884            Item::Command(Command {
885                name: "name".to_string(),
886                line_number: 0,
887                attributes: [("0".to_string(), "Ok".to_string())].into(),
888            }),
889            Item::Command(Command {
890                name: "print".to_string(),
891                line_number: 0,
892                attributes: [("data".to_string(), "Test".to_string())].into(),
893            }),
894            Item::Command(Command {
895                name: "click".to_string(),
896                line_number: 0,
897                attributes: BTreeMap::new(),
898            }),
899        ]
900    )
901}