msg_tool\scripts\yuris/
txt.rs

1//! Yu-Ris scenario text file (.txt)
2use crate::scripts::base::*;
3use crate::types::*;
4use crate::utils::encoding::*;
5use anyhow::Result;
6use std::collections::HashMap;
7use std::ops::{Deref, DerefMut};
8use std::sync::Arc;
9use unicode_segmentation::UnicodeSegmentation;
10
11#[derive(Debug)]
12pub struct YurisTxtBuilder {}
13
14impl YurisTxtBuilder {
15    /// Creates a new instance of `YurisTxtBuilder`
16    pub const fn new() -> Self {
17        YurisTxtBuilder {}
18    }
19}
20
21impl ScriptBuilder for YurisTxtBuilder {
22    fn default_encoding(&self) -> Encoding {
23        Encoding::Cp932
24    }
25
26    fn build_script(
27        &self,
28        buf: Vec<u8>,
29        _filename: &str,
30        encoding: Encoding,
31        _archive_encoding: Encoding,
32        config: &ExtraConfig,
33        _archive: Option<&Box<dyn Script>>,
34    ) -> Result<Box<dyn Script + Send + Sync>> {
35        Ok(Box::new(YurisTxt::new(&buf, encoding, config)?))
36    }
37
38    fn extensions(&self) -> &'static [&'static str] {
39        &["txt"]
40    }
41
42    fn script_type(&self) -> &'static ScriptType {
43        &ScriptType::YurisTxt
44    }
45}
46
47trait INode {
48    fn serialize(&self) -> String;
49}
50
51#[derive(Clone, Debug, PartialEq)]
52struct CommentNode(String);
53
54impl Deref for CommentNode {
55    type Target = String;
56
57    fn deref(&self) -> &Self::Target {
58        &self.0
59    }
60}
61
62impl DerefMut for CommentNode {
63    fn deref_mut(&mut self) -> &mut Self::Target {
64        &mut self.0
65    }
66}
67
68impl INode for CommentNode {
69    fn serialize(&self) -> String {
70        format!("//{}", self.0)
71    }
72}
73
74#[derive(Clone, Debug, PartialEq)]
75struct LabelNode(String);
76
77impl Deref for LabelNode {
78    type Target = String;
79
80    fn deref(&self) -> &Self::Target {
81        &self.0
82    }
83}
84
85impl DerefMut for LabelNode {
86    fn deref_mut(&mut self) -> &mut Self::Target {
87        &mut self.0
88    }
89}
90
91impl INode for LabelNode {
92    fn serialize(&self) -> String {
93        format!("#{}", self.0)
94    }
95}
96
97#[derive(Clone, Debug, PartialEq)]
98struct CommandNode {
99    name: String,
100    args: Vec<String>,
101    has_args: bool,
102}
103
104impl INode for CommandNode {
105    fn serialize(&self) -> String {
106        if !self.has_args {
107            return format!("\\{}", self.name);
108        }
109        let mut s = format!("\\{}(", self.name);
110        let mut first = true;
111        for arg in &self.args {
112            if first {
113                first = false;
114            } else {
115                s.push_str(", ");
116            }
117            if (arg.contains(" ") && !arg.chars().all(|s| s.is_ascii_digit() || s == ' '))
118                || arg.contains(",")
119                || arg.contains("=")
120            {
121                s.push_str(&format!("\"{}\"", arg));
122            } else {
123                s.push_str(arg);
124            }
125        }
126        s.push(')');
127        s
128    }
129}
130
131#[derive(Clone, Debug, PartialEq)]
132struct NameNode(String);
133
134impl Deref for NameNode {
135    type Target = String;
136    fn deref(&self) -> &Self::Target {
137        &self.0
138    }
139}
140
141impl DerefMut for NameNode {
142    fn deref_mut(&mut self) -> &mut Self::Target {
143        &mut self.0
144    }
145}
146
147impl INode for NameNode {
148    fn serialize(&self) -> String {
149        format!("【{}】", self.0)
150    }
151}
152
153#[derive(Clone, Debug, PartialEq)]
154struct TextNode(String);
155
156impl Deref for TextNode {
157    type Target = String;
158    fn deref(&self) -> &Self::Target {
159        &self.0
160    }
161}
162
163impl DerefMut for TextNode {
164    fn deref_mut(&mut self) -> &mut Self::Target {
165        &mut self.0
166    }
167}
168
169impl INode for TextNode {
170    fn serialize(&self) -> String {
171        self.0.clone()
172    }
173}
174
175#[derive(Clone, Debug, PartialEq)]
176struct CommentBlock(String);
177
178impl Deref for CommentBlock {
179    type Target = String;
180    fn deref(&self) -> &Self::Target {
181        &self.0
182    }
183}
184
185impl DerefMut for CommentBlock {
186    fn deref_mut(&mut self) -> &mut Self::Target {
187        &mut self.0
188    }
189}
190
191impl INode for CommentBlock {
192    fn serialize(&self) -> String {
193        format!("/*{}*/", self.0)
194    }
195}
196
197#[derive(Clone, Debug, PartialEq)]
198enum LineNode {
199    Comment(CommentNode),
200    Comments(CommentBlock),
201    Command(CommandNode),
202    Name(NameNode),
203    Text(TextNode),
204}
205
206impl INode for LineNode {
207    fn serialize(&self) -> String {
208        match self {
209            Self::Comment(node) => node.serialize(),
210            Self::Comments(node) => node.serialize(),
211            Self::Command(node) => node.serialize(),
212            Self::Name(node) => node.serialize(),
213            Self::Text(node) => node.serialize(),
214        }
215    }
216}
217
218impl INode for Vec<LineNode> {
219    fn serialize(&self) -> String {
220        self.iter()
221            .map(|s| s.serialize())
222            .collect::<Vec<_>>()
223            .join("")
224    }
225}
226
227#[derive(Clone, Debug, PartialEq)]
228enum Line {
229    Line(Vec<LineNode>),
230    Empty,
231    Label(LabelNode),
232}
233
234impl INode for Line {
235    fn serialize(&self) -> String {
236        match self {
237            Self::Line(line) => line.serialize(),
238            Self::Empty => String::new(),
239            Self::Label(label) => label.serialize(),
240        }
241    }
242}
243
244impl INode for Vec<Line> {
245    fn serialize(&self) -> String {
246        self.iter()
247            .map(|s| s.serialize())
248            .collect::<Vec<_>>()
249            .join("\n")
250    }
251}
252
253#[derive(Debug)]
254struct Parser<'a> {
255    lines: std::str::Lines<'a>,
256    cur_line: &'a str,
257    cur_pos: usize,
258    line_num: u64,
259    cur_line_chars: Vec<&'a str>,
260}
261
262impl<'a> Parser<'a> {
263    fn new(data: &'a str) -> Self {
264        Self {
265            lines: data.lines(),
266            cur_line: "",
267            cur_pos: 0,
268            line_num: 0,
269            cur_line_chars: Vec::new(),
270        }
271    }
272
273    fn error(&self, msg: impl std::fmt::Display) -> anyhow::Error {
274        anyhow::anyhow!("{} at line {} char {}", msg, self.line_num, self.cur_pos)
275    }
276
277    fn parse(mut self) -> Result<Vec<Line>> {
278        let mut lines = Vec::new();
279        while let Some(line) = self.lines.next() {
280            self.line_num += 1;
281            self.cur_line = line;
282            self.cur_line_chars = line.graphemes(true).collect();
283            lines.push(self.parse_line()?);
284        }
285        Ok(lines)
286    }
287
288    fn add_next_line(&mut self) -> Result<()> {
289        let next_line = self
290            .lines
291            .next()
292            .ok_or_else(|| anyhow::anyhow!("Unexpected eof"))?;
293        self.line_num += 1;
294        self.cur_line = next_line;
295        self.cur_line_chars.push("\n");
296        self.cur_line_chars.extend(next_line.graphemes(true));
297        Ok(())
298    }
299
300    fn parse_line(&mut self) -> Result<Line> {
301        self.cur_pos = 0;
302        if self.cur_line.trim_matches(' ').is_empty() {
303            return Ok(Line::Empty);
304        }
305        let mut line = Vec::new();
306        let mut text = String::new();
307        while let Some(c) = self.peek_char() {
308            // Skip space if text is empty
309            if text.is_empty() && (c == " " || c == "\t") {
310                self.cur_pos += 1;
311                continue;
312            }
313            // Label
314            if line.is_empty() && c == "#" {
315                self.cur_pos += 1;
316                let label = self.cur_line_chars[self.cur_pos..].join("");
317                return Ok(Line::Label(LabelNode(label)));
318            }
319            if c == "/" {
320                // Comment
321                if let Some(c) = self.peek_char_offset(1) {
322                    if c == "/" {
323                        let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
324                        if !ctext.is_empty() {
325                            line.push(LineNode::Text(TextNode(ctext.to_owned())));
326                            text.clear();
327                        }
328                        self.cur_pos += 2;
329                        let comment = self.cur_line_chars[self.cur_pos..].join("");
330                        line.push(LineNode::Comment(CommentNode(comment)));
331                        break;
332                    } else if c == "*" {
333                        let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
334                        if !ctext.is_empty() {
335                            line.push(LineNode::Text(TextNode(ctext.to_owned())));
336                            text.clear();
337                        }
338                        self.cur_pos += 2;
339                        let start_pos = self.cur_pos;
340                        let mut ok = false;
341                        loop {
342                            while let Some(c) = self.next_char() {
343                                if c == "*" && self.peek_char().is_some_and(|c| c == "/") {
344                                    let end_pos = self.cur_pos - 1;
345                                    self.cur_pos += 1;
346                                    ok = true;
347                                    line.push(LineNode::Comments(CommentBlock(
348                                        self.cur_line_chars[start_pos..end_pos].join(""),
349                                    )));
350                                    break;
351                                }
352                            }
353                            if ok {
354                                break;
355                            }
356                            self.add_next_line()?;
357                        }
358                        continue;
359                    }
360                }
361            }
362            // command
363            if c == "\\" {
364                let cmd = self.parse_command()?;
365                if !cmd.has_args && cmd.name == "R" {
366                    text.push_str("\\R");
367                    continue;
368                }
369                let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
370                if !ctext.is_empty() {
371                    line.push(LineNode::Text(TextNode(ctext.to_owned())));
372                    text.clear();
373                }
374                line.push(LineNode::Command(cmd));
375                continue;
376            }
377            // name
378            if c == "【" {
379                let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
380                if !ctext.is_empty() {
381                    line.push(LineNode::Text(TextNode(ctext.to_owned())));
382                    text.clear();
383                }
384                line.push(LineNode::Name(self.parse_name()?));
385                continue;
386            }
387            text.push_str(c);
388            self.cur_pos += 1;
389        }
390        let ctext = text.trim_end_matches(' ').trim_end_matches('\t');
391        if !ctext.is_empty() {
392            line.push(LineNode::Text(TextNode(ctext.to_owned())));
393        }
394        Ok(Line::Line(line))
395    }
396
397    fn parse_command(&mut self) -> Result<CommandNode> {
398        let c = self
399            .next_char_with_line()
400            .ok_or_else(|| self.error("Unexpected end of file"))?;
401        if c != "\\" {
402            return Err(self.error("Unexpected command start token"));
403        }
404        let mut name = String::new();
405        let mut args = Vec::new();
406        let mut in_quote = false;
407        let mut arg = String::new();
408        let mut ok = false;
409        while let Some(c) = self.peek_char() {
410            if c == "(" {
411                ok = true;
412                self.cur_pos += 1;
413                break;
414            }
415            if c == ")" {
416                return Err(self.error("Unexpected ) when parsing command"));
417            }
418            if !c.is_ascii() {
419                break;
420            }
421            name.push_str(c);
422            self.cur_pos += 1;
423            continue;
424        }
425        if !ok {
426            return Ok(CommandNode {
427                name: name.trim_matches(' ').trim_matches('\t').to_owned(),
428                args: Vec::new(),
429                has_args: false,
430            });
431        }
432        loop {
433            let c = self
434                .next_char_with_line()
435                .ok_or_else(|| self.error("Unexpected end of file when parsing command"))?;
436            if in_quote {
437                if c == "\"" {
438                    in_quote = false;
439                    continue;
440                }
441            } else {
442                if c == "\"" {
443                    in_quote = true;
444                    continue;
445                }
446                if c == "\n" || c == "\r\n" {
447                    continue;
448                }
449                if c == " " || c == "\t" {
450                    if arg.is_empty() {
451                        continue;
452                    }
453                    let mut tmp = c.to_string();
454                    while let Some(c) = self.peek_char() {
455                        if c == " " || c == "\t" {
456                            self.cur_pos += 1;
457                            tmp.push_str(c);
458                        } else if c == "," || c == ")" {
459                            break;
460                        } else {
461                            arg.push_str(&tmp);
462                            break;
463                        }
464                    }
465                    continue;
466                }
467                if c == "\\" {
468                    args.push(arg);
469                    self.cur_pos -= 1;
470                    return Ok(CommandNode {
471                        name: name.trim_matches(' ').trim_matches('\t').to_owned(),
472                        args,
473                        has_args: true,
474                    });
475                }
476                if c == "," {
477                    args.push(arg);
478                    arg = String::new();
479                    continue;
480                }
481                if c == ")" {
482                    args.push(arg);
483                    return Ok(CommandNode {
484                        name: name.trim_matches(' ').trim_matches('\t').to_owned(),
485                        args,
486                        has_args: true,
487                    });
488                }
489            }
490            arg.push_str(c);
491        }
492    }
493
494    fn parse_name(&mut self) -> Result<NameNode> {
495        let c = self
496            .next_char()
497            .ok_or_else(|| self.error("Unexpected end of line"))?;
498        if c != "【" {
499            return Err(self.error("Unexpected command start token"));
500        }
501        let mut name = String::new();
502        loop {
503            let c = self
504                .next_char()
505                .ok_or_else(|| self.error("Unexpected end of line when parsing name"))?;
506            if c == "】" {
507                return Ok(NameNode(name));
508            }
509            name.push_str(c);
510        }
511    }
512
513    fn peek_char(&self) -> Option<&'a str> {
514        self.cur_line_chars.get(self.cur_pos).map(|s| *s)
515    }
516
517    fn peek_char_offset(&self, offset: isize) -> Option<&'a str> {
518        let target = (self.cur_pos as isize + offset as isize) as usize;
519        self.cur_line_chars.get(target).map(|s| *s)
520    }
521
522    fn next_char(&mut self) -> Option<&'a str> {
523        let t = self.cur_line_chars.get(self.cur_pos).map(|s| *s);
524        if t.is_some() {
525            self.cur_pos += 1;
526        }
527        t
528    }
529
530    fn next_char_with_line(&mut self) -> Option<&'a str> {
531        let t = self.cur_line_chars.get(self.cur_pos).map(|s| *s);
532        if t.is_some() {
533            self.cur_pos += 1;
534            return t;
535        }
536        if self.add_next_line().is_err() {
537            return None;
538        }
539        self.next_char()
540    }
541}
542
543#[derive(Debug)]
544pub struct YurisTxt {
545    data: Vec<Line>,
546    bom: BomType,
547    tips_map: Option<Arc<HashMap<String, String>>>,
548}
549
550impl YurisTxt {
551    pub fn new<D: AsRef<[u8]> + ?Sized>(
552        data: &D,
553        encoding: Encoding,
554        config: &ExtraConfig,
555    ) -> Result<Self> {
556        let (text, bom) = decode_with_bom_detect(encoding, data.as_ref(), true)?;
557        let data = Parser::new(&text).parse()?;
558        Ok(Self {
559            data,
560            bom,
561            tips_map: config.yuris_tips_map.clone(),
562        })
563    }
564}
565
566impl Script for YurisTxt {
567    fn default_output_script_type(&self) -> OutputScriptType {
568        OutputScriptType::Json
569    }
570
571    fn default_format_type(&self) -> FormatOptions {
572        FormatOptions::None
573    }
574
575    fn extract_messages(&self) -> Result<Vec<Message>> {
576        let mut messages = Vec::new();
577        for line in &self.data {
578            if let Line::Line(line) = line {
579                let mut name = None;
580                let mut message = String::new();
581                for node in line.iter() {
582                    if let LineNode::Name(n) = node {
583                        name = Some(n.as_str());
584                    } else if let LineNode::Text(text) = node {
585                        message.push_str(&text.replace("\\R", "\n"));
586                    } else if let LineNode::Command(cmd) = node {
587                        if !message.is_empty() {
588                            message.push_str(&cmd.serialize());
589                        }
590                        if cmd.name == "SEL" {
591                            for arg in &cmd.args {
592                                messages.push(Message::new(arg.to_owned(), None));
593                            }
594                        }
595                    }
596                }
597                if !message.is_empty() {
598                    messages.push(Message::new(message, name.map(|s| s.to_owned())));
599                }
600            }
601        }
602        Ok(messages)
603    }
604
605    fn import_messages<'a>(
606        &'a self,
607        messages: Vec<Message>,
608        mut file: Box<dyn WriteSeek + 'a>,
609        _filename: &str,
610        encoding: Encoding,
611        replacement: Option<&'a ReplacementTable>,
612    ) -> Result<()> {
613        let mut data = self.data.clone();
614        let mut mess = messages.iter();
615        let mut mes = mess.next();
616        for line in data.iter_mut() {
617            if let Line::Line(line) = line {
618                let mut name_index = None;
619                let mut message_index = None;
620                for (i, node) in line.iter_mut().enumerate() {
621                    if let LineNode::Name(_) = node {
622                        name_index = Some(i);
623                    } else if let LineNode::Text(_) = node {
624                        if message_index.is_none() {
625                            message_index = Some(i);
626                        }
627                    } else if let LineNode::Command(cmd) = node {
628                        if cmd.name == "SEL" {
629                            for arg in cmd.args.iter_mut() {
630                                let mut m = mes
631                                    .ok_or_else(|| anyhow::anyhow!("No more messages to import"))?
632                                    .message
633                                    .clone();
634                                mes = mess.next();
635                                if let Some(rep) = replacement {
636                                    for (k, v) in &rep.map {
637                                        m = m.replace(k, v);
638                                    }
639                                }
640                                *arg = m;
641                            }
642                        } else if cmd.name == "TIPS.SET" {
643                            if cmd.args.len() >= 1 {
644                                if let Some(tips) = self.tips_map.as_ref() {
645                                    if let Some(data) = tips.get(&cmd.args[0]) {
646                                        let mut m = data.to_owned();
647                                        if let Some(rep) = replacement {
648                                            for (k, v) in &rep.map {
649                                                m = m.replace(k, v);
650                                            }
651                                        }
652                                        cmd.args[0] = m;
653                                    }
654                                }
655                            }
656                        }
657                    }
658                }
659                if let Some(message_idx) = message_index {
660                    let m = mes.ok_or_else(|| anyhow::anyhow!("No more messages to import"))?;
661                    mes = mess.next();
662                    if let Some(name_idx) = name_index {
663                        let mut name = m
664                            .name
665                            .as_ref()
666                            .ok_or_else(|| anyhow::anyhow!("Message don't have name"))?
667                            .clone();
668                        if let Some(rep) = replacement {
669                            for (k, v) in &rep.map {
670                                name = name.replace(k, v);
671                            }
672                        }
673                        if let LineNode::Name(n) = &mut line[name_idx] {
674                            n.0 = name;
675                        }
676                    }
677                    let mut m = m.message.replace("\n", "\\R");
678                    if let Some(rep) = replacement {
679                        for (k, v) in &rep.map {
680                            m = m.replace(k, v);
681                        }
682                    }
683                    let data = Parser::new(&m).parse()?;
684                    if data.len() != 1 {
685                        anyhow::bail!("parsed length is not 1.");
686                    }
687                    let li = data[0].clone();
688                    match li {
689                        Line::Label(_) => anyhow::bail!("Unsupported"),
690                        Line::Empty => {
691                            line.splice(message_idx.., []);
692                        }
693                        Line::Line(li) => {
694                            line.splice(message_idx.., li);
695                        }
696                    }
697                }
698            }
699        }
700        if mes.is_some() || mess.next().is_some() {
701            return Err(anyhow::anyhow!("Some messages were not processed."));
702        }
703        let data = data.serialize();
704        let data = encode_string_with_bom(encoding, &data, false, self.bom)?;
705        file.write_all(&data)?;
706        Ok(())
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    #[test]
714    fn test_parse1() {
715        let data = "\\T( , 250 ) \t【name】\t「……なんて」\t";
716        assert_eq!(
717            Parser::new(data).parse().unwrap(),
718            vec![Line::Line(vec![
719                LineNode::Command(CommandNode {
720                    name: "T".into(),
721                    args: vec!["".into(), "250".into()],
722                    has_args: true,
723                }),
724                LineNode::Name(NameNode("name".into())),
725                LineNode::Text(TextNode("「……なんて」".into())),
726            ])]
727        );
728    }
729    #[test]
730    fn test_parse2() {
731        let data = "\\T(2 5 \t0\t ) //TEST\n\\T ( \"250 \" , \"Wor,ks\" )";
732        assert_eq!(
733            Parser::new(data).parse().unwrap(),
734            vec![
735                Line::Line(vec![
736                    LineNode::Command(CommandNode {
737                        name: "T".into(),
738                        args: vec!["2 5 \t0".into()],
739                        has_args: true,
740                    }),
741                    LineNode::Comment(CommentNode("TEST".into()))
742                ]),
743                Line::Line(vec![LineNode::Command(CommandNode {
744                    name: "T".into(),
745                    args: vec!["250 ".into(), "Wor,ks".into()],
746                    has_args: true,
747                }),])
748            ]
749        );
750    }
751    #[test]
752    fn test_parse3() {
753        let data = "\\VO(UDA_0_ALL_0007_0004)【ウダツ】「んで、昨日あの後どうしたん? 実習の日程はもう決まった\\Rのか?」";
754        assert_eq!(
755            Parser::new(data).parse().unwrap(),
756            vec![Line::Line(vec![
757                LineNode::Command(CommandNode {
758                    name: "VO".into(),
759                    args: vec!["UDA_0_ALL_0007_0004".into()],
760                    has_args: true,
761                }),
762                LineNode::Name(NameNode("ウダツ".into())),
763                LineNode::Text(TextNode(
764                    "「んで、昨日あの後どうしたん? 実習の日程はもう決まった\\Rのか?」".into()
765                )),
766            ])]
767        );
768    }
769    #[test]
770    fn test_parse4() {
771        let data = "\\GO.TITLE";
772        assert_eq!(
773            Parser::new(data).parse().unwrap(),
774            vec![Line::Line(vec![LineNode::Command(CommandNode {
775                name: "GO.TITLE".into(),
776                args: vec![],
777                has_args: false,
778            }),])]
779        );
780    }
781    #[test]
782    fn test_parse5() {
783        let data = r"TEST/*
784\FOUT(600, 42, white)
785\BG.CMXYZ(  402,    0, -45)
786\BG(bg51 , 260, 0, 0)
787\PSET(回想フレーム, 0)
788\FIN(600, 41)
789*/Test";
790        assert_eq!(
791            Parser::new(data).parse().unwrap(),
792            vec![Line::Line(vec![
793                LineNode::Text(TextNode("TEST".into())),
794                LineNode::Comments(CommentBlock(
795                    r"
796\FOUT(600, 42, white)
797\BG.CMXYZ(  402,    0, -45)
798\BG(bg51 , 260, 0, 0)
799\PSET(回想フレーム, 0)
800\FIN(600, 41)
801"
802                    .into()
803                )),
804                LineNode::Text(TextNode("Test".into())),
805            ])]
806        );
807    }
808    #[test]
809    fn test_parse6() {
810        let data = r"\S.D(HASIRA
811)";
812        assert_eq!(
813            Parser::new(data).parse().unwrap(),
814            vec![Line::Line(vec![LineNode::Command(CommandNode {
815                name: "S.D".into(),
816                args: vec!["HASIRA".into()],
817                has_args: true
818            })])],
819        );
820    }
821    #[test]
822    fn test_parse7() {
823        let data = r"\S.CLXYZ(d,500,0,-40,0,1,1
824
825\VO(KAG_0_ALL_4010_0016)【カグヤ】「皆さんっ、すぐそうやって! ひとつしかない大切な身体なんですから今日みたいなことは……」";
826        assert_eq!(
827            Parser::new(data).parse().unwrap(),
828            vec![
829                Line::Line(vec![
830                    LineNode::Command(CommandNode {
831                        name: "S.CLXYZ".into(),
832                        args: vec![
833                            "d".into(),
834                            "500".into(),
835                            "0".into(),
836                            "-40".into(),
837                            "0".into(),
838                            "1".into(),
839                            "1".into()
840                        ],
841                        has_args: true
842                    }),
843                    LineNode::Command(CommandNode {
844                        name: "VO".into(),
845                        args: vec!["KAG_0_ALL_4010_0016".into()],
846                        has_args: true,
847                    }),
848                    LineNode::Name(NameNode("カグヤ".into())),
849                    LineNode::Text(TextNode("「皆さんっ、すぐそうやって! ひとつしかない大切な身体なんですから今日みたいなことは……」".into())),
850                ])
851            ],
852        );
853    }
854    #[test]
855    fn test_parse8() {
856        let data = r"\RP.END(SCENE_ETC_H04)";
857        assert_eq!(
858            Parser::new(data).parse().unwrap(),
859            vec![Line::Line(vec![LineNode::Command(CommandNode {
860                name: "RP.END".into(),
861                args: vec!["SCENE_ETC_H04".into()],
862                has_args: true
863            })])],
864        );
865    }
866    #[test]
867    fn test_ser1() {
868        assert_eq!(
869            (CommandNode {
870                name: "EV.CMXYZ".into(),
871                args: vec!["0  474".into(), "54".into(), "-58".into()],
872                has_args: true,
873            })
874            .serialize(),
875            r"\EV.CMXYZ(0  474, 54, -58)"
876        );
877    }
878}