1use crate::prelude::*;
5use serde::{Deserialize, Serialize};
6
7use super::{CharacterId, ChoiceOption, Directive, LineId, MusicLine, Scene, Side, Step};
8use crate::error::ContentError;
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct ParsedScene<D = super::NoDirective> {
13 pub scene: Scene<D>,
15 pub file: String,
17 pub line: u32,
19 pub lines: Lines,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
26pub struct Lines {
27 pub steps: Vec<u32>,
29 pub blocks: Vec<Vec<PartLines>>,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
39pub struct PartLines {
40 pub line: u32,
43 pub lines: Lines,
45}
46
47enum Open<D> {
49 Choice {
51 line: u32,
53 options: Vec<ChoiceOption<D>>,
54 parts: Vec<PartLines>,
55 outer_delivery: Option<String>,
58 },
59 If {
61 line: u32,
63 character: CharacterId,
64 then: (Vec<Step<D>>, Lines),
65 otherwise: Option<(u32, Vec<Step<D>>, Lines)>,
67 outer_delivery: Option<String>,
70 },
71}
72
73impl<D> Open<D> {
74 fn unclosed(&self) -> (u32, &'static str) {
76 match self {
77 Open::Choice { line, .. } => (*line, "@choice has no @endchoice"),
78 Open::If { line, .. } => (*line, "@if has no @endif"),
79 }
80 }
81}
82
83const REACTION_INDENT: usize = 2;
85
86pub fn parse_dlg<D: Directive>(
91 file: &str,
92 source: &str,
93) -> (Vec<ParsedScene<D>>, Vec<ContentError>) {
94 let mut p = Parser {
95 file,
96 scenes: Vec::new(),
97 open: None,
98 blocks: Vec::new(),
99 continuable: false,
100 delivery: None,
101 errors: Vec::new(),
102 };
103 for (i, line) in source.split('\n').enumerate() {
104 p.line(u32::try_from(i + 1).unwrap_or(u32::MAX), line);
105 }
106 p.close_unclosed();
107 if let Some(open) = p.open.take() {
108 p.no_end(&open);
109 p.finish(open);
110 }
111 (p.scenes, p.errors)
112}
113
114pub fn is_id(s: &str) -> bool {
117 !s.is_empty()
118 && s.bytes()
119 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
120}
121
122pub fn char_problem(c: char) -> Option<String> {
127 c.is_control()
128 .then(|| format!("control character U+{:04X} in dialogue text", u32::from(c)))
129}
130
131pub fn bad_id(id: &str) -> String {
133 format!("\"{id}\" is not a valid id; use lowercase letters, digits and _")
134}
135
136struct Parser<'a, D> {
137 file: &'a str,
138 scenes: Vec<ParsedScene<D>>,
139 open: Option<ParsedScene<D>>,
141 blocks: Vec<Open<D>>,
144 continuable: bool,
147 delivery: Option<String>,
150 errors: Vec<ContentError>,
151}
152
153impl<D: Directive> Parser<'_, D> {
154 fn err(&mut self, line: u32, message: impl Into<String>) {
155 self.errors
156 .push(ContentError::new(self.file, message).at(line, None));
157 }
158
159 fn no_end(&mut self, open: &ParsedScene<D>) {
160 self.err(
161 open.line,
162 format!("scene \"{}\" has no @end", open.scene.id),
163 );
164 }
165
166 fn finish(&mut self, mut open: ParsedScene<D>) {
170 open.scene.assign_line_ids();
171 self.scenes.push(open);
172 }
173
174 fn line(&mut self, n: u32, raw: &str) {
176 let line = raw.trim_end();
177 let body = line.trim_start_matches(' ');
178 if body.is_empty() || body.starts_with('#') {
179 return;
180 }
181 let indent = line.len() - body.len();
182 if self.in_choice() {
183 match indent {
184 0 => self.choice_line(n, line, body),
185 1..REACTION_INDENT => self.err(n, "reaction lines are indented by two spaces"),
186 REACTION_INDENT => self.statement(n, line, body),
187 _ => self.continuation(n, line, body),
188 }
189 return;
190 }
191 if indent > 0 {
192 self.continuation(n, line, body);
193 return;
194 }
195 if body.starts_with('*') {
196 self.err(
197 n,
198 "an option (\"* tone: text\") must be inside a @choice block",
199 );
200 return;
201 }
202 self.statement(n, line, body);
203 }
204
205 fn statement(&mut self, n: u32, line: &str, body: &str) {
208 self.continuable = false;
209 if let Some(rest) = body.strip_prefix('@') {
210 self.directive(n, line, rest);
211 } else if let Some(rest) = body.strip_prefix('>') {
212 let rest = rest.trim();
213 let directed = rest.strip_prefix('(').and_then(|r| r.split_once("):"));
215 let (own, text) = directed.map_or((None, rest), |(own, text)| (Some(own), text.trim()));
216 if text.is_empty() {
217 self.err(n, "narration has no text");
218 } else {
219 self.check_chars(n, line, text);
220 let direction = self.direction(n, own);
221 self.push(
222 n,
223 Step::Narrate {
224 text: text.into(),
225 direction,
226 line: LineId::default(),
227 },
228 );
229 self.continuable = self.open.is_some();
230 }
231 } else {
232 self.speech(n, line, body);
233 }
234 }
235
236 fn choice_line(&mut self, n: u32, line: &str, body: &str) {
239 self.continuable = false;
240 let name = body.split(' ').next().unwrap_or("");
241 let option = body.strip_prefix('*');
242 let ends = matches!(name, "@endchoice" | "@choice" | "@end" | "@scene");
243 if option.is_none() && !ends {
244 self.err(
245 n,
246 "inside @choice, a line is an option (\"* tone: text\"), an indented reaction line or @endchoice",
247 );
248 return;
249 }
250 self.close_reaction_ifs();
252 if let Some(Open::Choice { outer_delivery, .. }) = self.blocks.last() {
253 self.delivery.clone_from(outer_delivery);
254 }
255 match option {
256 Some(rest) => self.option(n, line, rest),
257 None => self.directive(n, line, &body[1..]),
258 }
259 }
260
261 fn in_choice(&self) -> bool {
263 self.blocks.iter().any(|b| matches!(b, Open::Choice { .. }))
264 }
265
266 fn close_reaction_ifs(&mut self) {
269 let is_if = |block: &mut Open<D>| matches!(block, Open::If { .. });
270 while let Some(block) = self.blocks.pop_if(is_if) {
271 let (line, message) = block.unclosed();
272 self.err(line, message);
273 self.add_block(block);
274 }
275 }
276
277 fn option(&mut self, n: u32, line: &str, rest: &str) {
279 let parsed = rest
280 .strip_prefix(' ')
281 .and_then(|r| r.split_once(':'))
282 .map(|(tone, text)| (tone.trim(), text.trim()))
283 .filter(|(tone, text)| !tone.is_empty() && !text.is_empty());
284 let Some((tone, text)) = parsed else {
285 self.err(n, "an option needs a tone and text: \"* tone: text\"");
286 return;
287 };
288 if !is_id(tone) {
289 self.bad_id(n, tone);
290 }
291 self.check_chars(n, line, text);
292 if let Some(Open::Choice { options, parts, .. }) = self.blocks.last_mut() {
293 options.push(ChoiceOption {
294 tone: tone.into(),
295 text: text.into(),
296 line: LineId::default(),
297 steps: Vec::new(),
298 });
299 parts.push(PartLines {
300 line: n,
301 lines: Lines::default(),
302 });
303 }
304 }
305
306 fn open_choice(&mut self, n: u32, args: &str) {
308 if !args.is_empty() {
309 self.err(n, "@choice takes nothing after it");
310 }
311 if self.in_choice() {
312 self.err(n, "@choice can't be nested inside another @choice");
313 return;
314 }
315 if self.open.is_none() {
316 self.err(n, "line is outside a scene; start one with @scene <id>");
317 return;
318 }
319 self.blocks.push(Open::Choice {
320 line: n,
321 options: Vec::new(),
322 parts: Vec::new(),
323 outer_delivery: self.delivery.clone(),
324 });
325 }
326
327 fn end_choice(&mut self, n: u32, args: &str) {
329 if !args.is_empty() {
330 self.err(n, "@endchoice takes nothing after it");
331 }
332 if !self.in_choice() {
333 self.err(n, "@endchoice without an open @choice");
334 return;
335 }
336 self.close_reaction_ifs();
337 if let Some(choice) = self.blocks.pop() {
338 self.add_block(choice);
339 }
340 }
341
342 fn open_if(&mut self, n: u32, args: &str) {
344 let words: Vec<&str> = args.split_whitespace().collect();
345 let character = if let [character] = words[..] {
346 if !is_id(character) {
347 self.bad_id(n, character);
348 }
349 character
350 } else {
351 self.err(n, "@if needs one character id");
352 ""
353 };
354 if self.target().is_none() {
355 self.no_target(n);
356 return;
357 }
358 self.blocks.push(Open::If {
359 line: n,
360 character: CharacterId(character.into()),
361 then: (Vec::new(), Lines::default()),
362 otherwise: None,
363 outer_delivery: self.delivery.clone(),
364 });
365 }
366
367 fn else_if(&mut self, n: u32, args: &str) {
369 if !args.is_empty() {
370 self.err(n, "@else takes nothing after it");
371 }
372 match self.blocks.last_mut() {
373 Some(Open::If {
374 otherwise: Some((first, ..)),
375 ..
376 }) => {
377 let message = format!("@if already has an @else, on line {first}");
378 self.err(n, message);
379 }
380 Some(Open::If {
381 otherwise,
382 outer_delivery,
383 ..
384 }) => {
385 *otherwise = Some((n, Vec::new(), Lines::default()));
386 self.delivery.clone_from(outer_delivery);
387 }
388 _ => self.err(n, "@else without an open @if"),
389 }
390 }
391
392 fn end_if(&mut self, n: u32, args: &str) {
394 if !args.is_empty() {
395 self.err(n, "@endif takes nothing after it");
396 }
397 match self.blocks.pop_if(|block| matches!(block, Open::If { .. })) {
398 Some(block) => self.add_block(block),
399 None => self.err(n, "@endif without an open @if"),
400 }
401 }
402
403 fn add_block(&mut self, block: Open<D>) {
406 let (line, step, parts) = match block {
407 Open::Choice {
408 line,
409 options,
410 parts,
411 outer_delivery,
412 } => {
413 self.delivery = outer_delivery;
414 (line, Step::Choice { options }, parts)
415 }
416 Open::If {
417 line,
418 character,
419 then,
420 otherwise,
421 outer_delivery,
422 } => {
423 self.delivery = outer_delivery;
424 let (else_line, otherwise, else_lines) =
425 otherwise.unwrap_or((line, Vec::new(), Lines::default()));
426 let step = Step::If {
427 character,
428 then: then.0,
429 otherwise,
430 };
431 let part = |line, lines| PartLines { line, lines };
432 (
433 line,
434 step,
435 vec![part(line, then.1), part(else_line, else_lines)],
436 )
437 }
438 };
439 if let Some((steps, lines)) = self.target() {
440 steps.push(step);
441 lines.steps.push(line);
442 lines.blocks.push(parts);
443 }
444 }
445
446 fn close_unclosed(&mut self) {
449 while let Some(block) = self.blocks.pop() {
450 let (line, message) = block.unclosed();
451 self.err(line, message);
452 self.add_block(block);
453 }
454 }
455
456 fn target(&mut self) -> Option<(&mut Vec<Step<D>>, &mut Lines)> {
461 match self.blocks.last_mut() {
462 Some(Open::If {
463 otherwise: Some((_, steps, lines)),
464 ..
465 }) => Some((steps, lines)),
466 Some(Open::If { then, .. }) => Some((&mut then.0, &mut then.1)),
467 Some(Open::Choice { options, parts, .. }) => {
468 match (options.last_mut(), parts.last_mut()) {
469 (Some(option), Some(part)) => Some((&mut option.steps, &mut part.lines)),
470 _ => None,
471 }
472 }
473 None => self
474 .open
475 .as_mut()
476 .map(|open| (&mut open.scene.steps, &mut open.lines)),
477 }
478 }
479
480 fn no_target(&mut self, n: u32) {
482 let message = if self.blocks.is_empty() {
483 "line is outside a scene; start one with @scene <id>"
484 } else {
485 "a reaction line needs an option above it: \"* tone: text\""
486 };
487 self.err(n, message);
488 }
489
490 fn last_step_mut(&mut self) -> Option<&mut Step<D>> {
492 self.target().and_then(|(steps, _)| steps.last_mut())
493 }
494
495 fn continuation(&mut self, n: u32, line: &str, body: &str) {
497 if body.starts_with('@') {
498 self.err(n, "directives can't be indented");
499 return;
500 }
501 let continuable = self.continuable;
502 let last = self.last_step_mut().filter(|_| continuable);
503 match last {
504 Some(Step::Say { text, .. } | Step::Narrate { text, .. }) => {
505 text.push(' ');
506 text.push_str(body);
507 self.check_chars(n, line, body);
508 }
509 _ => self.err(
510 n,
511 "indented line doesn't continue a speech or narration line",
512 ),
513 }
514 }
515
516 fn directive(&mut self, n: u32, line: &str, rest: &str) {
518 let (name, args) = rest
519 .split_once(' ')
520 .map_or((rest, ""), |(name, args)| (name, args.trim()));
521 match name {
522 "scene" => self.scene(n, args),
523 "end" => self.end(n, args),
524 "caption" if args.is_empty() => self.err(n, "@caption has no text"),
525 "caption" => {
526 self.check_chars(n, line, args);
527 self.push(n, Step::Caption { text: args.into() });
528 }
529 "left" => self.place(n, Side::Left, args),
530 "right" => self.place(n, Side::Right, args),
531 "music" => self.music(n, args),
532 "delivery" => self.delivery_line(n, args),
533 "choice" => self.open_choice(n, args),
534 "endchoice" => self.end_choice(n, args),
535 "if" => self.open_if(n, args),
536 "else" => self.else_if(n, args),
537 "endif" => self.end_if(n, args),
538 _ => match D::parse(name, args) {
539 Some(parsed) => self.game_directive(n, parsed),
540 None => self.err(n, format!("unknown directive \"@{name}\"")),
541 },
542 }
543 }
544
545 fn game_directive(&mut self, n: u32, parsed: super::Parsed<D>) {
548 for error in parsed.errors {
549 self.err(n, error);
550 }
551 if let Some(step) = parsed.step {
552 self.push(n, Step::Directive(step));
553 }
554 }
555
556 fn scene(&mut self, n: u32, args: &str) {
557 self.close_unclosed();
558 if let Some(open) = self.open.take() {
559 self.no_end(&open);
560 self.finish(open);
561 }
562 let ok = args.split_whitespace().count() == 1;
563 if !ok {
564 self.err(n, "@scene needs one scene id");
565 } else if !is_id(args) {
566 self.bad_id(n, args);
567 }
568 self.open = Some(ParsedScene {
569 scene: Scene {
570 id: args.into(),
571 steps: Vec::new(),
572 },
573 file: self.file.into(),
574 line: n,
575 lines: Lines::default(),
576 });
577 self.delivery = None;
578 }
579
580 fn end(&mut self, n: u32, args: &str) {
581 if !args.is_empty() {
582 self.err(n, "@end takes nothing after it");
583 }
584 self.close_unclosed();
585 match self.open.take() {
586 Some(open) => self.finish(open),
587 None => self.err(n, "@end without an open @scene"),
588 }
589 }
590
591 fn place(&mut self, n: u32, side: Side, args: &str) {
593 let args: Vec<&str> = args.split_whitespace().collect();
594 let step = match args[..] {
595 ["clear"] => Step::Clear { side },
596 [character, expression] if character != "clear" => {
597 if !is_id(character) {
598 self.bad_id(n, character);
599 }
600 if !is_id(expression) {
601 self.bad_id(n, expression);
602 }
603 Step::Place {
604 side,
605 character: CharacterId(character.into()),
606 expression: expression.into(),
607 }
608 }
609 _ => {
610 self.err(
611 n,
612 format!(
613 "@{} needs a character id and an expression, or \"clear\"",
614 side.name()
615 ),
616 );
617 return;
618 }
619 };
620 self.push(n, step);
621 }
622
623 fn music(&mut self, n: u32, args: &str) {
625 let args: Vec<&str> = args.split_whitespace().collect();
626 let [arg] = args[..] else {
627 self.err(n, "@music needs one music cue id, or \"stop\"");
628 return;
629 };
630 let music = if arg == MusicLine::STOP {
631 MusicLine::Stop
632 } else {
633 if !is_id(arg) {
634 self.bad_id(n, arg);
635 }
636 MusicLine::Cue(arg.into())
637 };
638 self.push(n, Step::Music(music));
639 }
640
641 fn speech(&mut self, n: u32, line: &str, body: &str) {
643 let split = body.split_once(':').and_then(|(head, text)| {
646 let Some((id, own)) = head.split_once(' ') else {
647 return Some((head, None, text));
648 };
649 let own = own.trim().strip_prefix('(')?.strip_suffix(')')?;
650 Some((id, Some(own), text))
651 });
652 let Some((head, own, text)) = split else {
653 self.err(
654 n,
655 "unrecognised line; expected \"@directive\", \"> narration\", \"speaker: text\" or \"# comment\"",
656 );
657 return;
658 };
659 let (speaker, expression) = match head.strip_suffix(']').and_then(|h| h.split_once('[')) {
660 Some((speaker, expression)) => (speaker, Some(expression)),
661 None => (head, None),
662 };
663 for id in core::iter::once(speaker).chain(expression) {
664 if !is_id(id) {
665 self.bad_id(n, id);
666 }
667 }
668 let text = text.trim();
669 if text.is_empty() {
670 self.err(n, format!("\"{head}:\" has no text"));
671 return;
672 }
673 self.check_chars(n, line, text);
674 let direction = self.direction(n, own);
675 self.push(
676 n,
677 Step::Say {
678 speaker: CharacterId(speaker.into()),
679 expression: expression.map(Into::into),
680 text: text.into(),
681 direction,
682 line: LineId::default(),
683 },
684 );
685 self.continuable = self.open.is_some();
686 }
687
688 fn direction(&mut self, n: u32, own: Option<&str>) -> Option<String> {
691 match own {
692 Some(own) => self.checked_direction(n, own, "a line's ( ) has no direction in it"),
693 None => self.delivery.clone(),
694 }
695 }
696
697 fn checked_direction(&mut self, n: u32, direction: &str, when_empty: &str) -> Option<String> {
700 let direction = direction.trim();
701 if direction.is_empty() {
702 self.err(n, when_empty);
703 return None;
704 }
705 for message in direction.chars().filter_map(char_problem) {
706 self.err(n, message);
707 }
708 Some(direction.into())
709 }
710
711 fn delivery_line(&mut self, n: u32, args: &str) {
713 if self.target().is_none() {
714 self.no_target(n);
715 return;
716 }
717 self.delivery = if args == "clear" {
718 None
719 } else {
720 self.checked_direction(n, args, "@delivery needs a direction, or \"clear\"")
721 };
722 }
723
724 fn bad_id(&mut self, n: u32, id: &str) {
725 self.err(n, bad_id(id));
726 }
727
728 fn push(&mut self, n: u32, step: Step<D>) {
731 match self.target() {
732 Some((steps, lines)) => {
733 steps.push(step);
734 lines.steps.push(n);
735 }
736 None => self.no_target(n),
737 }
738 }
739
740 fn check_chars(&mut self, n: u32, line: &str, text: &str) {
743 let before = line[..line.len() - text.len()].chars().count();
744 for (i, c) in text.chars().enumerate() {
745 if let Some(message) = char_problem(c) {
746 let column = u32::try_from(before + i + 1).unwrap_or(u32::MAX);
747 self.errors
748 .push(ContentError::new(self.file, message).at(n, Some(column)));
749 }
750 }
751 }
752}