1use crate::Lexer;
34use crate::ast::{
35 Chord, CommentStyle, Directive, DirectiveKind, ImageAttributes, Line, LyricsLine,
36 LyricsSegment, Song,
37};
38use crate::inline_markup;
39use crate::token::{Span, Token, TokenKind};
40
41#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ParseError {
51 pub message: String,
53 pub span: Span,
55}
56
57impl ParseError {
58 fn new(message: impl Into<String>, span: Span) -> Self {
60 Self {
61 message: message.into(),
62 span,
63 }
64 }
65
66 #[must_use]
68 pub fn line(&self) -> usize {
69 self.span.start.line
70 }
71
72 #[must_use]
74 pub fn column(&self) -> usize {
75 self.span.start.column
76 }
77}
78
79impl core::fmt::Display for ParseError {
80 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
81 write!(
82 f,
83 "parse error at line {}, column {}: {}",
84 self.span.start.line, self.span.start.column, self.message
85 )
86 }
87}
88
89impl std::error::Error for ParseError {}
90
91#[derive(Debug, Clone)]
113pub struct ParseResult {
114 pub song: Song,
116 pub errors: Vec<ParseError>,
118}
119
120impl ParseResult {
121 #[must_use]
123 pub fn is_ok(&self) -> bool {
124 self.errors.is_empty()
125 }
126
127 #[must_use]
129 pub fn has_errors(&self) -> bool {
130 !self.errors.is_empty()
131 }
132}
133
134pub struct Parser {
144 tokens: Vec<Token>,
146 pos: usize,
148 verbatim_end: Option<String>,
152}
153
154impl Parser {
155 #[must_use]
163 pub fn new(tokens: Vec<Token>) -> Self {
164 assert!(
165 !tokens.is_empty(),
166 "token list must contain at least an Eof token"
167 );
168 Self {
169 tokens,
170 pos: 0,
171 verbatim_end: None,
172 }
173 }
174
175 #[must_use = "callers must handle the parse result"]
185 pub fn parse(mut self) -> Result<Song, ParseError> {
186 let mut song = Song::new();
187
188 while !self.is_at_end() {
189 let line = self.parse_line()?;
190
191 if let Line::Directive(ref directive) = line {
195 if directive.selector.is_none() {
196 Self::populate_metadata(&mut song.metadata, directive);
197 }
198 }
199
200 song.lines.push(line);
201 }
202
203 song.apply_define_displays();
204 Ok(song)
205 }
206
207 #[must_use = "callers must handle the parse result"]
215 pub fn parse_lenient(self) -> ParseResult {
216 self.parse_lenient_limited(0)
217 }
218
219 #[must_use = "callers must handle the parse result"]
222 pub fn parse_lenient_limited(mut self, max_errors: usize) -> ParseResult {
223 let mut song = Song::new();
224 let mut errors = Vec::new();
225
226 while !self.is_at_end() {
227 match self.parse_line() {
228 Ok(line) => {
229 if let Line::Directive(ref directive) = line {
230 if directive.selector.is_none() {
231 Self::populate_metadata(&mut song.metadata, directive);
232 }
233 }
234 song.lines.push(line);
235 }
236 Err(e) => {
237 if max_errors == 0 || errors.len() < max_errors {
238 errors.push(e);
239 }
240 self.skip_to_next_line();
242 }
243 }
244 }
245
246 song.apply_define_displays();
247 ParseResult { song, errors }
248 }
249
250 fn skip_to_next_line(&mut self) {
253 while !self.is_at_end() {
254 if self.peek_kind() == &TokenKind::Newline {
255 self.advance();
256 return;
257 }
258 self.advance();
259 }
260 }
261
262 const MAX_METADATA_ENTRIES: usize = 1000;
268
269 fn push_if_under_cap<T>(vec: &mut Vec<T>, value: T) {
272 if vec.len() < Self::MAX_METADATA_ENTRIES {
273 vec.push(value);
274 }
275 }
276
277 pub fn populate_metadata(metadata: &mut crate::ast::Metadata, directive: &Directive) {
283 let value = match directive.value.as_deref() {
284 Some(v) => v.to_string(),
285 None => return, };
287
288 match directive.kind {
289 DirectiveKind::Title => {
290 metadata.title = Some(value);
291 }
292 DirectiveKind::Subtitle => {
293 Self::push_if_under_cap(&mut metadata.subtitles, value);
294 }
295 DirectiveKind::Artist => {
296 Self::push_if_under_cap(&mut metadata.artists, value);
297 }
298 DirectiveKind::Composer => {
299 Self::push_if_under_cap(&mut metadata.composers, value);
300 }
301 DirectiveKind::Lyricist => {
302 Self::push_if_under_cap(&mut metadata.lyricists, value);
303 }
304 DirectiveKind::Album => {
305 metadata.album = Some(value);
306 }
307 DirectiveKind::Year => {
308 metadata.year = Some(value);
309 }
310 DirectiveKind::Key => {
311 metadata.key = Some(value);
312 }
313 DirectiveKind::Tempo => {
314 metadata.tempo = Some(value);
315 }
316 DirectiveKind::Time => {
317 metadata.time = Some(value);
318 }
319 DirectiveKind::Capo => {
320 metadata.capo = Some(value);
321 }
322 DirectiveKind::SortTitle => {
323 metadata.sort_title = Some(value);
324 }
325 DirectiveKind::SortArtist => {
326 metadata.sort_artist = Some(value);
327 }
328 DirectiveKind::Arranger => {
329 Self::push_if_under_cap(&mut metadata.arrangers, value);
330 }
331 DirectiveKind::Copyright => {
332 metadata.copyright = Some(value);
333 }
334 DirectiveKind::Duration => {
335 metadata.duration = Some(value);
336 }
337 DirectiveKind::Tag => {
338 Self::push_if_under_cap(&mut metadata.tags, value);
339 }
340 DirectiveKind::Meta(ref key) => match key.to_ascii_lowercase().as_str() {
341 "title" | "t" => metadata.title = Some(value),
342 "subtitle" | "st" => Self::push_if_under_cap(&mut metadata.subtitles, value),
343 "artist" => Self::push_if_under_cap(&mut metadata.artists, value),
344 "composer" => Self::push_if_under_cap(&mut metadata.composers, value),
345 "lyricist" => Self::push_if_under_cap(&mut metadata.lyricists, value),
346 "album" => metadata.album = Some(value),
347 "year" => metadata.year = Some(value),
348 "key" => metadata.key = Some(value),
349 "tempo" => metadata.tempo = Some(value),
350 "time" => metadata.time = Some(value),
351 "capo" => metadata.capo = Some(value),
352 "sorttitle" => metadata.sort_title = Some(value),
353 "sortartist" => metadata.sort_artist = Some(value),
354 "arranger" => Self::push_if_under_cap(&mut metadata.arrangers, value),
355 "copyright" => metadata.copyright = Some(value),
356 "duration" => metadata.duration = Some(value),
357 "tag" => Self::push_if_under_cap(&mut metadata.tags, value),
358 _ => Self::push_if_under_cap(&mut metadata.custom, (key.clone(), value)),
359 },
360 DirectiveKind::Unknown(ref name) => {
361 Self::push_if_under_cap(&mut metadata.custom, (name.clone(), value));
362 }
363 _ => {}
364 }
365 }
366
367 fn is_at_end(&self) -> bool {
371 self.pos >= self.tokens.len() || self.peek_kind() == &TokenKind::Eof
372 }
373
374 fn peek_kind(&self) -> &TokenKind {
376 self.tokens
377 .get(self.pos)
378 .map(|t| &t.kind)
379 .unwrap_or(&TokenKind::Eof)
380 }
381
382 fn peek(&self) -> &Token {
384 &self.tokens[self.pos]
387 }
388
389 fn advance(&mut self) -> &Token {
391 let tok = &self.tokens[self.pos];
392 self.pos += 1;
393 tok
394 }
395
396 fn parse_line(&mut self) -> Result<Line, ParseError> {
400 let in_verbatim = self.verbatim_end.is_some();
401
402 match self.peek_kind() {
403 TokenKind::Newline => {
405 self.advance();
406 Ok(Line::Empty)
407 }
408 TokenKind::DirectiveOpen => {
410 if in_verbatim && !self.is_verbatim_end_ahead() {
413 return self.parse_verbatim_line();
414 }
415 let line = self.parse_directive_line()?;
416 if let Line::Directive(ref d) = line {
418 if let Some(end_name) = Self::verbatim_end_for(&d.kind) {
419 self.verbatim_end = Some(end_name);
420 } else if d.kind.is_section_end() && in_verbatim {
421 self.verbatim_end = None;
422 }
423 }
424 Ok(line)
425 }
426 _ if in_verbatim => self.parse_verbatim_line(),
428 TokenKind::Text(t) if t.starts_with('#') => self.parse_hash_comment_line(),
432 _ => self.parse_lyrics_line(),
434 }
435 }
436
437 fn verbatim_end_for(kind: &DirectiveKind) -> Option<String> {
441 match kind {
442 DirectiveKind::StartOfTab => Some("end_of_tab".to_string()),
443 DirectiveKind::StartOfGrid => Some("end_of_grid".to_string()),
444 DirectiveKind::StartOfAbc => Some("end_of_abc".to_string()),
445 DirectiveKind::StartOfLy => Some("end_of_ly".to_string()),
446 DirectiveKind::StartOfSvg => Some("end_of_svg".to_string()),
447 DirectiveKind::StartOfTextblock => Some("end_of_textblock".to_string()),
448 DirectiveKind::StartOfMusicxml => Some("end_of_musicxml".to_string()),
449 _ => None,
450 }
451 }
452
453 fn is_verbatim_end_ahead(&self) -> bool {
461 if let Some(ref end_name) = self.verbatim_end {
462 if self.pos + 1 < self.tokens.len() {
463 if let TokenKind::Text(ref text) = self.tokens[self.pos + 1].kind {
464 let trimmed = text.trim().to_ascii_lowercase();
465 if trimmed == *end_name {
467 return true;
468 }
469 return match end_name.as_str() {
471 "end_of_tab" => trimmed == "eot",
472 "end_of_grid" => trimmed == "eog",
473 _ => false,
474 };
475 }
476 }
477 }
478 false
479 }
480
481 fn parse_verbatim_line(&mut self) -> Result<Line, ParseError> {
487 let text = self.collect_raw_line();
488
489 if self.peek_kind() == &TokenKind::Newline {
491 self.advance();
492 }
493
494 if text.is_empty() {
495 Ok(Line::Empty)
496 } else {
497 Ok(Line::Lyrics(LyricsLine {
498 segments: vec![LyricsSegment::text_only(text)],
499 }))
500 }
501 }
502
503 fn parse_hash_comment_line(&mut self) -> Result<Line, ParseError> {
512 let raw = self.collect_raw_line();
513
514 if self.peek_kind() == &TokenKind::Newline {
516 self.advance();
517 }
518
519 let after_hash = raw
522 .strip_prefix('#')
523 .expect("dispatch guard guarantees raw starts with '#'");
524 let text = after_hash.strip_prefix(' ').unwrap_or(after_hash);
525
526 Ok(Line::Comment(CommentStyle::Normal, text.to_string()))
527 }
528
529 fn collect_raw_line(&mut self) -> String {
536 let mut raw = String::new();
537 loop {
538 match self.peek_kind() {
539 TokenKind::Newline | TokenKind::Eof => break,
540 TokenKind::Text(t) => {
541 raw.push_str(t);
542 self.advance();
543 }
544 TokenKind::ChordOpen => {
545 raw.push('[');
546 self.advance();
547 }
548 TokenKind::ChordClose => {
549 raw.push(']');
550 self.advance();
551 }
552 TokenKind::DirectiveOpen => {
553 raw.push('{');
554 self.advance();
555 }
556 TokenKind::DirectiveClose => {
557 raw.push('}');
558 self.advance();
559 }
560 TokenKind::Colon => {
561 raw.push(':');
562 self.advance();
563 }
564 }
565 }
566 raw
567 }
568
569 fn parse_directive_line(&mut self) -> Result<Line, ParseError> {
577 let open_span = self.peek().span;
578 self.advance(); let name = self.parse_directive_name(&open_span)?;
582
583 let value = if self.peek_kind() == &TokenKind::Colon {
585 self.advance(); Some(self.parse_directive_value())
587 } else {
588 None
589 };
590
591 if self.peek_kind() != &TokenKind::DirectiveClose {
593 let span = self.peek().span;
594 return Err(ParseError::new("unclosed directive: expected `}`", span));
595 }
596 self.advance();
597
598 if self.peek_kind() == &TokenKind::Newline {
600 self.advance();
601 }
602
603 let name = name.trim().to_string();
605 let value = value.map(|v| v.trim().to_string());
606
607 let (kind, selector) = DirectiveKind::resolve_with_selector(&name);
609
610 if kind.is_comment() && selector.is_none() {
614 let style = match kind {
615 DirectiveKind::Comment => CommentStyle::Normal,
616 DirectiveKind::CommentItalic => CommentStyle::Italic,
617 DirectiveKind::CommentBox => CommentStyle::Boxed,
618 _ => CommentStyle::Normal,
619 };
620 let text = value.unwrap_or_default();
621 return Ok(Line::Comment(style, text));
622 }
623
624 if matches!(kind, DirectiveKind::Meta(_)) {
626 if let Some(ref val) = value {
627 let trimmed = val.trim();
628 if let Some(pos) = trimmed.find(|c: char| c.is_whitespace()) {
629 let meta_key = trimmed[..pos].to_string();
630 let meta_value = trimmed[pos..].trim().to_string();
631 let kind = DirectiveKind::Meta(meta_key.clone());
632 let directive = Directive {
633 name: "meta".to_string(),
634 value: if meta_value.is_empty() {
635 None
636 } else {
637 Some(meta_value)
638 },
639 kind,
640 selector,
641 };
642 return Ok(Line::Directive(directive));
643 } else if !trimmed.is_empty() {
644 let meta_key = trimmed.to_string();
646 let kind = DirectiveKind::Meta(meta_key);
647 let directive = Directive {
648 name: "meta".to_string(),
649 value: None,
650 kind,
651 selector,
652 };
653 return Ok(Line::Directive(directive));
654 }
655 }
656 let directive = Directive {
658 name: "meta".to_string(),
659 value: None,
660 kind: DirectiveKind::Unknown("meta".to_string()),
661 selector,
662 };
663 return Ok(Line::Directive(directive));
664 }
665
666 if kind.is_image() {
668 let attrs = match &value {
669 Some(v) => parse_image_attributes(v),
670 None => ImageAttributes::default(),
671 };
672 let kind = DirectiveKind::Image(attrs);
673 let canonical = kind.canonical_name().to_string();
674 let directive = Directive {
675 name: canonical,
676 value,
677 kind,
678 selector,
679 };
680 return Ok(Line::Directive(directive));
681 }
682
683 let canonical = kind.full_canonical_name();
685 let directive = Directive {
686 name: canonical,
687 value,
688 kind,
689 selector,
690 };
691
692 Ok(Line::Directive(directive))
693 }
694
695 fn parse_directive_name(&mut self, open_span: &Span) -> Result<String, ParseError> {
697 let mut name = String::new();
698
699 loop {
700 match self.peek_kind() {
701 TokenKind::Text(text) => {
702 name.push_str(text);
703 self.advance();
704 }
705 TokenKind::Colon | TokenKind::DirectiveClose => break,
706 TokenKind::Eof | TokenKind::Newline => {
707 return Err(ParseError::new(
708 "unclosed directive: expected `}`",
709 *open_span,
710 ));
711 }
712 _ => {
713 let tok = self.peek();
715 return Err(ParseError::new(
716 format!("unexpected {:?} in directive name", tok.kind),
717 tok.span,
718 ));
719 }
720 }
721 }
722
723 if name.trim().is_empty() {
724 return Err(ParseError::new("empty directive name", *open_span));
725 }
726
727 Ok(name)
728 }
729
730 fn parse_directive_value(&mut self) -> String {
735 let mut value = String::new();
736
737 loop {
738 match self.peek_kind() {
739 TokenKind::Text(text) => {
740 value.push_str(text);
741 self.advance();
742 }
743 TokenKind::DirectiveClose | TokenKind::Eof | TokenKind::Newline => break,
744 TokenKind::Colon => {
745 value.push(':');
747 self.advance();
748 }
749 TokenKind::ChordOpen => {
750 value.push('[');
751 self.advance();
752 }
753 TokenKind::ChordClose => {
754 value.push(']');
755 self.advance();
756 }
757 TokenKind::DirectiveOpen => {
758 value.push('{');
759 self.advance();
760 }
761 }
762 }
763
764 value
765 }
766
767 fn parse_lyrics_line(&mut self) -> Result<Line, ParseError> {
774 let mut segments: Vec<LyricsSegment> = Vec::new();
775 let mut current_chord: Option<Chord> = None;
776 let mut current_text = String::new();
777
778 loop {
779 match self.peek_kind() {
780 TokenKind::Newline | TokenKind::Eof => {
781 break;
782 }
783 TokenKind::ChordOpen => {
784 if current_chord.is_some() || !current_text.is_empty() {
786 segments.push(LyricsSegment::new(
787 current_chord.take(),
788 core::mem::take(&mut current_text),
789 ));
790 }
791
792 current_chord = Some(self.parse_chord()?);
793 }
794 TokenKind::Text(text) => {
795 current_text.push_str(text);
796 self.advance();
797 }
798 TokenKind::DirectiveOpen => {
799 current_text.push('{');
809 self.advance();
810 }
811 TokenKind::DirectiveClose => {
812 current_text.push('}');
814 self.advance();
815 }
816 TokenKind::ChordClose => {
817 current_text.push(']');
819 self.advance();
820 }
821 TokenKind::Colon => {
822 current_text.push(':');
826 self.advance();
827 }
828 }
829 }
830
831 if current_chord.is_some() || !current_text.is_empty() {
833 segments.push(LyricsSegment::new(current_chord, current_text));
834 }
835
836 if self.peek_kind() == &TokenKind::Newline {
838 self.advance();
839 }
840
841 if segments.is_empty() {
842 Ok(Line::Empty)
843 } else {
844 let segments = segments
846 .into_iter()
847 .map(Self::apply_inline_markup)
848 .collect();
849 Ok(Line::Lyrics(LyricsLine { segments }))
850 }
851 }
852
853 fn apply_inline_markup(mut segment: LyricsSegment) -> LyricsSegment {
860 if inline_markup::has_inline_markup(&segment.text) {
861 let spans = inline_markup::parse_inline_markup(&segment.text);
862 if !spans.is_empty() {
863 segment.text = inline_markup::spans_to_plain_text(&spans);
865 segment.spans = spans;
866 }
867 }
868 segment
869 }
870
871 fn parse_chord(&mut self) -> Result<Chord, ParseError> {
876 let open_span = self.peek().span;
877 self.advance(); let mut name = String::new();
880
881 loop {
882 match self.peek_kind() {
883 TokenKind::Text(text) => {
884 name.push_str(text);
885 self.advance();
886 }
887 TokenKind::ChordClose => {
888 self.advance(); break;
890 }
891 TokenKind::Newline | TokenKind::Eof => {
892 return Err(ParseError::new("unclosed chord: expected `]`", open_span));
893 }
894 _ => {
895 let tok = self.peek();
897 return Err(ParseError::new(
898 format!("unexpected {:?} inside chord", tok.kind),
899 tok.span,
900 ));
901 }
902 }
903 }
904
905 Ok(Chord::new(name))
906 }
907}
908
909#[must_use = "callers must handle the parse error"]
932pub fn parse(input: &str) -> Result<Song, ParseError> {
933 parse_with_options(input, &ParseOptions::default())
934}
935
936#[derive(Debug, Clone)]
938pub struct ParseOptions {
939 pub max_input_size: usize,
944
945 pub max_errors: usize,
952}
953
954impl Default for ParseOptions {
955 fn default() -> Self {
956 Self {
957 max_input_size: 10 * 1024 * 1024, max_errors: 1000,
959 }
960 }
961}
962
963#[must_use = "callers must handle the parse error"]
973pub fn parse_with_options(input: &str, options: &ParseOptions) -> Result<Song, ParseError> {
974 if options.max_input_size > 0 && input.len() > options.max_input_size {
975 return Err(ParseError::new(
976 format!(
977 "input size ({} bytes) exceeds maximum ({} bytes)",
978 input.len(),
979 options.max_input_size
980 ),
981 Span::new(
982 crate::token::Position::new(1, 1),
983 crate::token::Position::new(1, 1),
984 ),
985 ));
986 }
987 let tokens = Lexer::new(input).tokenize();
988 Parser::new(tokens).parse()
989}
990
991#[must_use]
1010pub fn parse_lenient(input: &str) -> ParseResult {
1011 parse_lenient_with_options(input, &ParseOptions::default())
1012}
1013
1014#[must_use]
1018pub fn parse_lenient_with_options(input: &str, options: &ParseOptions) -> ParseResult {
1019 if options.max_input_size > 0 && input.len() > options.max_input_size {
1020 return ParseResult {
1021 song: Song::new(),
1022 errors: vec![ParseError::new(
1023 format!(
1024 "input size ({} bytes) exceeds maximum ({} bytes)",
1025 input.len(),
1026 options.max_input_size
1027 ),
1028 Span::new(
1029 crate::token::Position::new(1, 1),
1030 crate::token::Position::new(1, 1),
1031 ),
1032 )],
1033 };
1034 }
1035 let tokens = Lexer::new(input).tokenize();
1036 Parser::new(tokens).parse_lenient_limited(options.max_errors)
1037}
1038
1039#[derive(Debug, Clone)]
1049pub struct MultiParseResult {
1050 pub results: Vec<ParseResult>,
1053}
1054
1055impl MultiParseResult {
1056 #[must_use]
1058 pub fn songs(&self) -> Vec<&Song> {
1059 self.results.iter().map(|r| &r.song).collect()
1060 }
1061
1062 #[must_use]
1064 pub fn is_ok(&self) -> bool {
1065 self.results.iter().all(|r| r.is_ok())
1066 }
1067
1068 #[must_use]
1070 pub fn has_errors(&self) -> bool {
1071 self.results.iter().any(|r| r.has_errors())
1072 }
1073
1074 #[must_use]
1076 pub fn all_errors(&self) -> Vec<&ParseError> {
1077 self.results.iter().flat_map(|r| r.errors.iter()).collect()
1078 }
1079}
1080
1081fn is_new_song_line(trimmed: &str) -> bool {
1087 if !trimmed.starts_with('{') || !trimmed.ends_with('}') {
1090 return false;
1091 }
1092 let inner = trimmed[1..trimmed.len() - 1].trim().to_ascii_lowercase();
1093 let name = match inner.find(':') {
1095 Some(pos) => inner[..pos].trim_end(),
1096 None => inner.as_str(),
1097 };
1098 name == "new_song" || name == "ns"
1099}
1100
1101fn split_at_new_song(input: &str) -> Vec<&str> {
1107 let mut segments = Vec::new();
1108 let mut seg_start = 0;
1109 let bytes = input.as_bytes();
1110 let len = bytes.len();
1111 let mut pos = 0;
1112
1113 while pos < len {
1114 let line_start = pos;
1115 while pos < len && bytes[pos] != b'\r' && bytes[pos] != b'\n' {
1117 pos += 1;
1118 }
1119 let line_end = pos;
1120 let after_newline = if pos < len && bytes[pos] == b'\r' {
1122 if pos + 1 < len && bytes[pos + 1] == b'\n' {
1123 pos + 2
1124 } else {
1125 pos + 1 }
1127 } else if pos < len && bytes[pos] == b'\n' {
1128 pos + 1
1129 } else {
1130 pos
1131 };
1132 pos = after_newline;
1133
1134 let line = &input[line_start..line_end];
1135 let trimmed = line.trim();
1136 if is_new_song_line(trimmed) {
1137 segments.push(&input[seg_start..line_start]);
1138 seg_start = after_newline;
1139 }
1140 }
1141
1142 segments.push(&input[seg_start..]);
1143 segments
1144}
1145
1146#[must_use = "callers must handle the parse error"]
1169pub fn parse_multi(input: &str) -> Result<Vec<Song>, ParseError> {
1170 parse_multi_with_options(input, &ParseOptions::default())
1171}
1172
1173#[must_use = "callers must handle the parse error"]
1184pub fn parse_multi_with_options(
1185 input: &str,
1186 options: &ParseOptions,
1187) -> Result<Vec<Song>, ParseError> {
1188 if options.max_input_size > 0 && input.len() > options.max_input_size {
1189 return Err(ParseError::new(
1190 format!(
1191 "input size ({} bytes) exceeds maximum ({} bytes)",
1192 input.len(),
1193 options.max_input_size
1194 ),
1195 Span::new(
1196 crate::token::Position::new(1, 1),
1197 crate::token::Position::new(1, 1),
1198 ),
1199 ));
1200 }
1201
1202 let segments = split_at_new_song(input);
1203 let mut songs = Vec::with_capacity(segments.len());
1204
1205 for segment in segments {
1206 let tokens = Lexer::new(segment).tokenize();
1207 let song = Parser::new(tokens).parse()?;
1208 songs.push(song);
1209 }
1210
1211 Ok(songs)
1212}
1213
1214#[must_use]
1233pub fn parse_multi_lenient(input: &str) -> MultiParseResult {
1234 parse_multi_lenient_with_options(input, &ParseOptions::default())
1235}
1236
1237#[must_use]
1241pub fn parse_multi_lenient_with_options(input: &str, options: &ParseOptions) -> MultiParseResult {
1242 if options.max_input_size > 0 && input.len() > options.max_input_size {
1243 return MultiParseResult {
1244 results: vec![ParseResult {
1245 song: Song::new(),
1246 errors: vec![ParseError::new(
1247 format!(
1248 "input size ({} bytes) exceeds maximum ({} bytes)",
1249 input.len(),
1250 options.max_input_size
1251 ),
1252 Span::new(
1253 crate::token::Position::new(1, 1),
1254 crate::token::Position::new(1, 1),
1255 ),
1256 )],
1257 }],
1258 };
1259 }
1260
1261 let segments = split_at_new_song(input);
1262 let results: Vec<ParseResult> = segments
1263 .into_iter()
1264 .map(|segment| {
1265 let tokens = Lexer::new(segment).tokenize();
1266 Parser::new(tokens).parse_lenient_limited(options.max_errors)
1267 })
1268 .collect();
1269
1270 MultiParseResult { results }
1271}
1272
1273const IMAGE_SRC_MAX_BYTES: usize = 4096;
1279
1280const IMAGE_ATTR_MAX_BYTES: usize = 1024;
1282
1283fn truncate_string(s: String, max_bytes: usize) -> String {
1286 if s.len() <= max_bytes {
1287 return s;
1288 }
1289 let mut end = max_bytes;
1290 while end > 0 && !s.is_char_boundary(end) {
1291 end -= 1;
1292 }
1293 s[..end].to_string()
1294}
1295
1296#[must_use]
1312pub fn parse_image_attributes(input: &str) -> ImageAttributes {
1313 let mut attrs = ImageAttributes::default();
1314 let pairs = split_key_value_pairs(input);
1315
1316 for (key, value) in pairs {
1317 match key.to_ascii_lowercase().as_str() {
1318 "src" => attrs.src = truncate_string(value, IMAGE_SRC_MAX_BYTES),
1319 "width" => attrs.width = Some(truncate_string(value, IMAGE_ATTR_MAX_BYTES)),
1320 "height" => attrs.height = Some(truncate_string(value, IMAGE_ATTR_MAX_BYTES)),
1321 "scale" => attrs.scale = Some(truncate_string(value, IMAGE_ATTR_MAX_BYTES)),
1322 "title" => attrs.title = Some(truncate_string(value, IMAGE_ATTR_MAX_BYTES)),
1323 "anchor" => attrs.anchor = Some(truncate_string(value, IMAGE_ATTR_MAX_BYTES)),
1324 _ => {
1325 }
1327 }
1328 }
1329
1330 attrs
1331}
1332
1333fn split_key_value_pairs(input: &str) -> Vec<(String, String)> {
1338 let mut pairs = Vec::new();
1339 let bytes = input.as_bytes();
1340 let len = bytes.len();
1341 let mut i = 0;
1342
1343 while i < len {
1344 while i < len && bytes[i].is_ascii_whitespace() {
1346 i += 1;
1347 }
1348 if i >= len {
1349 break;
1350 }
1351
1352 let key_start = i;
1354 while i < len && bytes[i] != b'=' && !bytes[i].is_ascii_whitespace() {
1355 i += 1;
1356 }
1357 let key = &input[key_start..i];
1358
1359 if i >= len || bytes[i] != b'=' {
1360 while i < len && !bytes[i].is_ascii_whitespace() {
1363 i += 1;
1364 }
1365 continue;
1366 }
1367
1368 i += 1;
1370
1371 let value = if i < len && bytes[i] == b'"' {
1373 i += 1; let val_start = i;
1376 while i < len && bytes[i] != b'"' {
1377 i += 1;
1378 }
1379 let val = &input[val_start..i];
1380 if i < len {
1381 i += 1; }
1383 val
1384 } else {
1385 let val_start = i;
1387 while i < len && !bytes[i].is_ascii_whitespace() {
1388 i += 1;
1389 }
1390 &input[val_start..i]
1391 };
1392
1393 if !key.is_empty() {
1394 pairs.push((key.to_string(), value.to_string()));
1395 }
1396 }
1397
1398 pairs
1399}
1400
1401#[cfg(test)]
1406mod tests {
1407 use super::*;
1408 use crate::ast::{
1409 Chord, CommentStyle, Directive, DirectiveKind, Line, LyricsLine, LyricsSegment,
1410 };
1411
1412 fn lines(input: &str) -> Vec<Line> {
1416 parse(input).expect("parse failed").lines
1417 }
1418
1419 #[test]
1422 fn input_within_limit_succeeds() {
1423 let opts = ParseOptions {
1424 max_input_size: 100,
1425 ..Default::default()
1426 };
1427 let result = parse_with_options("{title: Test}", &opts);
1428 assert!(result.is_ok());
1429 }
1430
1431 #[test]
1432 fn input_exceeding_limit_fails() {
1433 let opts = ParseOptions {
1434 max_input_size: 10,
1435 ..Default::default()
1436 };
1437 let result = parse_with_options("{title: This is too long}", &opts);
1438 assert!(result.is_err());
1439 let err = result.unwrap_err();
1440 assert!(err.message.contains("exceeds maximum"));
1441 }
1442
1443 #[test]
1444 fn zero_limit_disables_check() {
1445 let opts = ParseOptions {
1446 max_input_size: 0,
1447 ..Default::default()
1448 };
1449 let result = parse_with_options("{title: Any size is fine}", &opts);
1450 assert!(result.is_ok());
1451 }
1452
1453 #[test]
1454 fn default_limit_is_10mb() {
1455 let opts = ParseOptions::default();
1456 assert_eq!(opts.max_input_size, 10 * 1024 * 1024);
1457 assert_eq!(opts.max_errors, 1000);
1458 }
1459
1460 #[test]
1463 fn empty_input() {
1464 let song = parse("").unwrap();
1465 assert!(song.lines.is_empty());
1466 }
1467
1468 #[test]
1471 fn single_empty_line() {
1472 let result = lines("\n");
1473 assert_eq!(result, vec![Line::Empty]);
1474 }
1475
1476 #[test]
1477 fn multiple_empty_lines() {
1478 let result = lines("\n\n\n");
1479 assert_eq!(result, vec![Line::Empty, Line::Empty, Line::Empty]);
1480 }
1481
1482 #[test]
1485 fn plain_text_line() {
1486 let result = lines("Hello world");
1487 assert_eq!(
1488 result,
1489 vec![Line::Lyrics(LyricsLine {
1490 segments: vec![LyricsSegment::text_only("Hello world")],
1491 })]
1492 );
1493 }
1494
1495 #[test]
1496 fn multiple_plain_text_lines() {
1497 let result = lines("Line one\nLine two");
1498 assert_eq!(
1499 result,
1500 vec![
1501 Line::Lyrics(LyricsLine {
1502 segments: vec![LyricsSegment::text_only("Line one")],
1503 }),
1504 Line::Lyrics(LyricsLine {
1505 segments: vec![LyricsSegment::text_only("Line two")],
1506 }),
1507 ]
1508 );
1509 }
1510
1511 #[test]
1514 fn single_chord_with_text() {
1515 let result = lines("[Am]Hello");
1516 assert_eq!(
1517 result,
1518 vec![Line::Lyrics(LyricsLine {
1519 segments: vec![LyricsSegment::new(Some(Chord::new("Am")), "Hello")],
1520 })]
1521 );
1522 }
1523
1524 #[test]
1525 fn multiple_chords_with_text() {
1526 let result = lines("[Am]Hello [G]world");
1527 assert_eq!(
1528 result,
1529 vec![Line::Lyrics(LyricsLine {
1530 segments: vec![
1531 LyricsSegment::new(Some(Chord::new("Am")), "Hello "),
1532 LyricsSegment::new(Some(Chord::new("G")), "world"),
1533 ],
1534 })]
1535 );
1536 }
1537
1538 #[test]
1539 fn chord_only_no_text() {
1540 let result = lines("[Am]");
1541 assert_eq!(
1542 result,
1543 vec![Line::Lyrics(LyricsLine {
1544 segments: vec![LyricsSegment::chord_only(Chord::new("Am"))],
1545 })]
1546 );
1547 }
1548
1549 #[test]
1550 fn consecutive_chords_no_text_between() {
1551 let result = lines("[Am][G]");
1552 assert_eq!(
1553 result,
1554 vec![Line::Lyrics(LyricsLine {
1555 segments: vec![
1556 LyricsSegment::chord_only(Chord::new("Am")),
1557 LyricsSegment::chord_only(Chord::new("G")),
1558 ],
1559 })]
1560 );
1561 }
1562
1563 #[test]
1564 fn text_before_first_chord() {
1565 let result = lines("Hello [Am]world");
1566 assert_eq!(
1567 result,
1568 vec![Line::Lyrics(LyricsLine {
1569 segments: vec![
1570 LyricsSegment::text_only("Hello "),
1571 LyricsSegment::new(Some(Chord::new("Am")), "world"),
1572 ],
1573 })]
1574 );
1575 }
1576
1577 #[test]
1578 fn chord_at_end_of_line() {
1579 let result = lines("Hello [Am]");
1580 assert_eq!(
1581 result,
1582 vec![Line::Lyrics(LyricsLine {
1583 segments: vec![
1584 LyricsSegment::text_only("Hello "),
1585 LyricsSegment::chord_only(Chord::new("Am")),
1586 ],
1587 })]
1588 );
1589 }
1590
1591 #[test]
1592 fn empty_chord_name() {
1593 let result = lines("[]text");
1595 assert_eq!(
1596 result,
1597 vec![Line::Lyrics(LyricsLine {
1598 segments: vec![LyricsSegment::new(Some(Chord::new("")), "text")],
1599 })]
1600 );
1601 }
1602
1603 #[test]
1606 fn directive_with_value() {
1607 let result = lines("{title: My Song}");
1608 assert_eq!(
1609 result,
1610 vec![Line::Directive(Directive::with_value("title", "My Song"))],
1611 );
1612 }
1613
1614 #[test]
1615 fn directive_without_value() {
1616 let result = lines("{start_of_chorus}");
1617 assert_eq!(
1618 result,
1619 vec![Line::Directive(Directive::name_only("start_of_chorus"))],
1620 );
1621 }
1622
1623 #[test]
1624 fn directive_value_trimmed() {
1625 let result = lines("{title: Hello World }");
1626 assert_eq!(
1627 result,
1628 vec![Line::Directive(Directive::with_value(
1629 "title",
1630 "Hello World"
1631 ))],
1632 );
1633 }
1634
1635 #[test]
1636 fn directive_name_trimmed() {
1637 let result = lines("{ title : value}");
1638 assert_eq!(
1639 result,
1640 vec![Line::Directive(Directive::with_value("title", "value"))],
1641 );
1642 }
1643
1644 #[test]
1645 fn directive_with_colon_in_value() {
1646 let result = lines("{comment: time 10:30}");
1648 assert_eq!(
1650 result,
1651 vec![Line::Comment(
1652 CommentStyle::Normal,
1653 "time 10:30".to_string()
1654 )]
1655 );
1656 }
1657
1658 #[test]
1659 fn directive_followed_by_lyrics() {
1660 let result = lines("{title: Test}\n[Am]Hello");
1661 assert_eq!(
1662 result,
1663 vec![
1664 Line::Directive(Directive::with_value("title", "Test")),
1665 Line::Lyrics(LyricsLine {
1666 segments: vec![LyricsSegment::new(Some(Chord::new("Am")), "Hello")],
1667 }),
1668 ]
1669 );
1670 }
1671
1672 #[test]
1675 fn comment_directive_full_name() {
1676 let result = lines("{comment: This is a comment}");
1677 assert_eq!(
1678 result,
1679 vec![Line::Comment(
1680 CommentStyle::Normal,
1681 "This is a comment".to_string()
1682 )],
1683 );
1684 }
1685
1686 #[test]
1687 fn comment_directive_short_name() {
1688 let result = lines("{c: Short comment}");
1689 assert_eq!(
1690 result,
1691 vec![Line::Comment(
1692 CommentStyle::Normal,
1693 "Short comment".to_string()
1694 )],
1695 );
1696 }
1697
1698 #[test]
1699 fn comment_directive_no_value() {
1700 let result = lines("{comment}");
1701 assert_eq!(
1702 result,
1703 vec![Line::Comment(CommentStyle::Normal, String::new())]
1704 );
1705 }
1706
1707 #[test]
1708 fn comment_italic_directive() {
1709 let result = lines("{comment_italic: Softly}");
1710 assert_eq!(
1711 result,
1712 vec![Line::Comment(CommentStyle::Italic, "Softly".to_string())],
1713 );
1714 }
1715
1716 #[test]
1717 fn comment_italic_short_name() {
1718 let result = lines("{ci: Softly}");
1719 assert_eq!(
1720 result,
1721 vec![Line::Comment(CommentStyle::Italic, "Softly".to_string())],
1722 );
1723 }
1724
1725 #[test]
1726 fn comment_box_directive() {
1727 let result = lines("{comment_box: Important}");
1728 assert_eq!(
1729 result,
1730 vec![Line::Comment(CommentStyle::Boxed, "Important".to_string())],
1731 );
1732 }
1733
1734 #[test]
1735 fn comment_box_short_name() {
1736 let result = lines("{cb: Important}");
1737 assert_eq!(
1738 result,
1739 vec![Line::Comment(CommentStyle::Boxed, "Important".to_string())],
1740 );
1741 }
1742
1743 #[test]
1746 fn hash_comment_basic() {
1747 let result = lines("# This is a comment");
1748 assert_eq!(
1749 result,
1750 vec![Line::Comment(
1751 CommentStyle::Normal,
1752 "This is a comment".to_string()
1753 )],
1754 );
1755 }
1756
1757 #[test]
1758 fn hash_comment_no_space_after_hash() {
1759 let result = lines("#no space");
1761 assert_eq!(
1762 result,
1763 vec![Line::Comment(CommentStyle::Normal, "no space".to_string())],
1764 );
1765 }
1766
1767 #[test]
1768 fn hash_comment_standalone_hash() {
1769 let result = lines("#");
1771 assert_eq!(
1772 result,
1773 vec![Line::Comment(CommentStyle::Normal, "".to_string())],
1774 );
1775 }
1776
1777 #[test]
1778 fn hash_comment_mixed_with_directives() {
1779 let result = lines("# First\n{title: My Song}\n# Second");
1781 assert_eq!(
1782 result,
1783 vec![
1784 Line::Comment(CommentStyle::Normal, "First".to_string()),
1785 Line::Directive(Directive::with_value("title", "My Song")),
1786 Line::Comment(CommentStyle::Normal, "Second".to_string()),
1787 ],
1788 );
1789 }
1790
1791 #[test]
1792 fn hash_comment_indented_is_lyrics_not_comment() {
1793 let result = lines(" # indented");
1796 assert!(
1797 matches!(result[..], [Line::Lyrics(_)]),
1798 "expected Lyrics, got {result:?}"
1799 );
1800 }
1801
1802 #[test]
1805 fn directive_short_alias_title() {
1806 let result = lines("{t: My Song}");
1807 let expected = Directive::with_value("title", "My Song");
1808 assert_eq!(result, vec![Line::Directive(expected)]);
1809 }
1810
1811 #[test]
1812 fn directive_short_alias_subtitle() {
1813 let result = lines("{st: Alternate}");
1814 let expected = Directive::with_value("subtitle", "Alternate");
1815 assert_eq!(result, vec![Line::Directive(expected)]);
1816 }
1817
1818 #[test]
1819 fn directive_short_alias_soc() {
1820 let result = lines("{soc}");
1821 let expected = Directive::name_only("start_of_chorus");
1822 assert_eq!(result, vec![Line::Directive(expected)]);
1823 }
1824
1825 #[test]
1826 fn directive_short_alias_eoc() {
1827 let result = lines("{eoc}");
1828 let expected = Directive::name_only("end_of_chorus");
1829 assert_eq!(result, vec![Line::Directive(expected)]);
1830 }
1831
1832 #[test]
1833 fn directive_case_insensitive() {
1834 let result = lines("{TITLE: Upper}");
1835 let expected = Directive::with_value("title", "Upper");
1836 assert_eq!(result, vec![Line::Directive(expected)]);
1837 }
1838
1839 #[test]
1840 fn directive_mixed_case() {
1841 let result = lines("{Start_Of_Chorus}");
1842 let expected = Directive::name_only("start_of_chorus");
1843 assert_eq!(result, vec![Line::Directive(expected)]);
1844 }
1845
1846 #[test]
1847 fn directive_unknown_preserved() {
1848 let result = lines("{my_custom: value}");
1849 assert_eq!(
1850 result,
1851 vec![Line::Directive(Directive {
1852 name: "my_custom".to_string(),
1853 value: Some("value".to_string()),
1854 kind: DirectiveKind::Unknown("my_custom".to_string()),
1855 selector: None,
1856 })],
1857 );
1858 }
1859
1860 #[test]
1861 fn directive_kind_on_parsed_directive() {
1862 let song = parse("{title: Test}").unwrap();
1863 if let Line::Directive(ref d) = song.lines[0] {
1864 assert_eq!(d.kind, DirectiveKind::Title);
1865 assert_eq!(d.name, "title");
1866 } else {
1867 panic!("expected directive");
1868 }
1869 }
1870
1871 #[test]
1874 fn environment_directives_long_form() {
1875 let cases = vec![
1876 (
1877 "{start_of_chorus}",
1878 "start_of_chorus",
1879 DirectiveKind::StartOfChorus,
1880 ),
1881 (
1882 "{end_of_chorus}",
1883 "end_of_chorus",
1884 DirectiveKind::EndOfChorus,
1885 ),
1886 (
1887 "{start_of_verse}",
1888 "start_of_verse",
1889 DirectiveKind::StartOfVerse,
1890 ),
1891 ("{end_of_verse}", "end_of_verse", DirectiveKind::EndOfVerse),
1892 (
1893 "{start_of_bridge}",
1894 "start_of_bridge",
1895 DirectiveKind::StartOfBridge,
1896 ),
1897 (
1898 "{end_of_bridge}",
1899 "end_of_bridge",
1900 DirectiveKind::EndOfBridge,
1901 ),
1902 ("{start_of_tab}", "start_of_tab", DirectiveKind::StartOfTab),
1903 ("{end_of_tab}", "end_of_tab", DirectiveKind::EndOfTab),
1904 ];
1905
1906 for (input, expected_name, expected_kind) in cases {
1907 let result = lines(input);
1908 if let Line::Directive(ref d) = result[0] {
1909 assert_eq!(d.name, expected_name, "failed for input: {input}");
1910 assert_eq!(d.kind, expected_kind, "failed for input: {input}");
1911 } else {
1912 panic!("expected directive for input: {input}");
1913 }
1914 }
1915 }
1916
1917 #[test]
1918 fn environment_directives_short_form() {
1919 let cases = vec![
1920 ("{soc}", "start_of_chorus", DirectiveKind::StartOfChorus),
1921 ("{eoc}", "end_of_chorus", DirectiveKind::EndOfChorus),
1922 ("{sov}", "start_of_verse", DirectiveKind::StartOfVerse),
1923 ("{eov}", "end_of_verse", DirectiveKind::EndOfVerse),
1924 ("{sob}", "start_of_bridge", DirectiveKind::StartOfBridge),
1925 ("{eob}", "end_of_bridge", DirectiveKind::EndOfBridge),
1926 ("{sot}", "start_of_tab", DirectiveKind::StartOfTab),
1927 ("{eot}", "end_of_tab", DirectiveKind::EndOfTab),
1928 ];
1929
1930 for (input, expected_name, expected_kind) in cases {
1931 let result = lines(input);
1932 if let Line::Directive(ref d) = result[0] {
1933 assert_eq!(d.name, expected_name, "failed for input: {input}");
1934 assert_eq!(d.kind, expected_kind, "failed for input: {input}");
1935 } else {
1936 panic!("expected directive for input: {input}");
1937 }
1938 }
1939 }
1940
1941 #[test]
1944 fn metadata_title_populated() {
1945 let song = parse("{title: Amazing Grace}").unwrap();
1946 assert_eq!(song.metadata.title.as_deref(), Some("Amazing Grace"));
1947 }
1948
1949 #[test]
1950 fn metadata_title_via_short_alias() {
1951 let song = parse("{t: Amazing Grace}").unwrap();
1952 assert_eq!(song.metadata.title.as_deref(), Some("Amazing Grace"));
1953 }
1954
1955 #[test]
1956 fn metadata_subtitle_populated() {
1957 let song = parse("{subtitle: How sweet}\n{st: The sound}").unwrap();
1958 assert_eq!(song.metadata.subtitles, vec!["How sweet", "The sound"]);
1959 }
1960
1961 #[test]
1962 fn metadata_artist_populated() {
1963 let song = parse("{artist: John Newton}").unwrap();
1964 assert_eq!(song.metadata.artists, vec!["John Newton"]);
1965 }
1966
1967 #[test]
1968 fn metadata_multiple_artists() {
1969 let song = parse("{artist: John}\n{artist: Jane}").unwrap();
1970 assert_eq!(song.metadata.artists, vec!["John", "Jane"]);
1971 }
1972
1973 #[test]
1974 fn metadata_composer_populated() {
1975 let song = parse("{composer: Bach}").unwrap();
1976 assert_eq!(song.metadata.composers, vec!["Bach"]);
1977 }
1978
1979 #[test]
1980 fn metadata_lyricist_populated() {
1981 let song = parse("{lyricist: Someone}").unwrap();
1982 assert_eq!(song.metadata.lyricists, vec!["Someone"]);
1983 }
1984
1985 #[test]
1986 fn metadata_album_populated() {
1987 let song = parse("{album: Greatest Hits}").unwrap();
1988 assert_eq!(song.metadata.album.as_deref(), Some("Greatest Hits"));
1989 }
1990
1991 #[test]
1992 fn metadata_year_populated() {
1993 let song = parse("{year: 1779}").unwrap();
1994 assert_eq!(song.metadata.year.as_deref(), Some("1779"));
1995 }
1996
1997 #[test]
1998 fn metadata_key_populated() {
1999 let song = parse("{key: G}").unwrap();
2000 assert_eq!(song.metadata.key.as_deref(), Some("G"));
2001 }
2002
2003 #[test]
2004 fn metadata_tempo_populated() {
2005 let song = parse("{tempo: 120}").unwrap();
2006 assert_eq!(song.metadata.tempo.as_deref(), Some("120"));
2007 }
2008
2009 #[test]
2010 fn metadata_time_populated() {
2011 let song = parse("{time: 3/4}").unwrap();
2012 assert_eq!(song.metadata.time.as_deref(), Some("3/4"));
2013 }
2014
2015 #[test]
2016 fn metadata_capo_populated() {
2017 let song = parse("{capo: 2}").unwrap();
2018 assert_eq!(song.metadata.capo.as_deref(), Some("2"));
2019 }
2020
2021 #[test]
2022 fn metadata_case_insensitive() {
2023 let song = parse("{TITLE: Upper Case}").unwrap();
2024 assert_eq!(song.metadata.title.as_deref(), Some("Upper Case"));
2025 }
2026
2027 #[test]
2028 fn metadata_not_populated_without_value() {
2029 let song = parse("{title}").unwrap();
2030 assert_eq!(song.metadata.title, None);
2031 }
2032
2033 #[test]
2034 fn metadata_all_fields_populated() {
2035 let input = "\
2036{title: My Song}
2037{subtitle: A Sub}
2038{artist: An Artist}
2039{composer: A Composer}
2040{lyricist: A Lyricist}
2041{album: An Album}
2042{year: 2024}
2043{key: Am}
2044{tempo: 100}
2045{time: 4/4}
2046{capo: 3}";
2047
2048 let song = parse(input).unwrap();
2049 assert_eq!(song.metadata.title.as_deref(), Some("My Song"));
2050 assert_eq!(song.metadata.subtitles, vec!["A Sub"]);
2051 assert_eq!(song.metadata.artists, vec!["An Artist"]);
2052 assert_eq!(song.metadata.composers, vec!["A Composer"]);
2053 assert_eq!(song.metadata.lyricists, vec!["A Lyricist"]);
2054 assert_eq!(song.metadata.album.as_deref(), Some("An Album"));
2055 assert_eq!(song.metadata.year.as_deref(), Some("2024"));
2056 assert_eq!(song.metadata.key.as_deref(), Some("Am"));
2057 assert_eq!(song.metadata.tempo.as_deref(), Some("100"));
2058 assert_eq!(song.metadata.time.as_deref(), Some("4/4"));
2059 assert_eq!(song.metadata.capo.as_deref(), Some("3"));
2060 }
2061
2062 #[test]
2063 fn metadata_custom_populated_for_unknown_directive() {
2064 let song = parse("{x_my_custom: some value}").unwrap();
2065 assert_eq!(
2066 song.metadata.custom,
2067 vec![("x_my_custom".to_string(), "some value".to_string())]
2068 );
2069 }
2070
2071 #[test]
2072 fn metadata_custom_multiple_unknown_directives() {
2073 let song = parse("{x_one: first}\n{x_two: second}").unwrap();
2074 assert_eq!(
2075 song.metadata.custom,
2076 vec![
2077 ("x_one".to_string(), "first".to_string()),
2078 ("x_two".to_string(), "second".to_string()),
2079 ]
2080 );
2081 }
2082
2083 #[test]
2084 fn metadata_custom_not_populated_without_value() {
2085 let song = parse("{x_no_value}").unwrap();
2086 assert!(song.metadata.custom.is_empty());
2087 }
2088
2089 #[test]
2090 fn metadata_custom_coexists_with_standard_metadata() {
2091 let input = "{title: My Song}\n{x_custom: custom value}";
2092 let song = parse(input).unwrap();
2093 assert_eq!(song.metadata.title.as_deref(), Some("My Song"));
2094 assert_eq!(
2095 song.metadata.custom,
2096 vec![("x_custom".to_string(), "custom value".to_string())]
2097 );
2098 }
2099
2100 #[test]
2103 fn unclosed_directive() {
2104 let err = parse("{title: oops").unwrap_err();
2105 assert!(
2106 err.message.contains("unclosed directive"),
2107 "error message was: {}",
2108 err.message
2109 );
2110 }
2111
2112 #[test]
2113 fn unclosed_chord() {
2114 let err = parse("[Am").unwrap_err();
2115 assert!(
2116 err.message.contains("unclosed chord"),
2117 "error message was: {}",
2118 err.message
2119 );
2120 }
2121
2122 #[test]
2123 fn empty_directive_name() {
2124 let err = parse("{}").unwrap_err();
2125 assert!(
2126 err.message.contains("empty directive name"),
2127 "error message was: {}",
2128 err.message
2129 );
2130 }
2131
2132 #[test]
2133 fn empty_directive_with_colon() {
2134 let err = parse("{: value}").unwrap_err();
2135 assert!(
2136 err.message.contains("empty directive name"),
2137 "error message was: {}",
2138 err.message
2139 );
2140 }
2141
2142 #[test]
2143 fn unclosed_chord_at_newline() {
2144 let err = parse("[Am\ntext").unwrap_err();
2145 assert!(
2146 err.message.contains("unclosed chord"),
2147 "error message was: {}",
2148 err.message
2149 );
2150 }
2151
2152 #[test]
2153 fn parse_error_display() {
2154 let err = parse("{title: no close").unwrap_err();
2155 let msg = format!("{err}");
2156 assert!(msg.contains("parse error at line"));
2157 assert!(msg.contains("unclosed directive"));
2158 }
2159
2160 #[test]
2163 fn full_song() {
2164 let input = "\
2165{title: Amazing Grace}
2166{artist: John Newton}
2167
2168[G]Amazing [G7]grace, how [C]sweet the [G]sound
2169[G]That saved a [Em]wretch like [D]me";
2170
2171 let song = parse(input).unwrap();
2172 assert_eq!(song.lines.len(), 5);
2173
2174 assert_eq!(song.metadata.title.as_deref(), Some("Amazing Grace"));
2176 assert_eq!(song.metadata.artists, vec!["John Newton"]);
2177
2178 assert_eq!(
2180 song.lines[0],
2181 Line::Directive(Directive::with_value("title", "Amazing Grace")),
2182 );
2183
2184 assert_eq!(
2186 song.lines[1],
2187 Line::Directive(Directive::with_value("artist", "John Newton")),
2188 );
2189
2190 assert_eq!(song.lines[2], Line::Empty);
2192
2193 if let Line::Lyrics(ref lyrics) = song.lines[3] {
2195 assert_eq!(lyrics.text(), "Amazing grace, how sweet the sound");
2196 assert!(lyrics.has_chords());
2197 assert_eq!(lyrics.segments.len(), 4);
2198 assert_eq!(lyrics.segments[0].chord.as_ref().unwrap().name, "G");
2199 assert_eq!(lyrics.segments[0].text, "Amazing ");
2200 assert_eq!(lyrics.segments[1].chord.as_ref().unwrap().name, "G7");
2201 assert_eq!(lyrics.segments[1].text, "grace, how ");
2202 assert_eq!(lyrics.segments[2].chord.as_ref().unwrap().name, "C");
2203 assert_eq!(lyrics.segments[2].text, "sweet the ");
2204 assert_eq!(lyrics.segments[3].chord.as_ref().unwrap().name, "G");
2205 assert_eq!(lyrics.segments[3].text, "sound");
2206 } else {
2207 panic!("expected Line::Lyrics for line 4");
2208 }
2209
2210 if let Line::Lyrics(ref lyrics) = song.lines[4] {
2212 assert_eq!(lyrics.text(), "That saved a wretch like me");
2213 assert_eq!(lyrics.segments.len(), 3);
2214 } else {
2215 panic!("expected Line::Lyrics for line 5");
2216 }
2217 }
2218
2219 #[test]
2220 fn song_with_sections() {
2221 let input = "\
2222{start_of_chorus}
2223[C]La la [G]la
2224{end_of_chorus}";
2225
2226 let song = parse(input).unwrap();
2227 assert_eq!(song.lines.len(), 3);
2228 assert!(matches!(song.lines[0], Line::Directive(_)));
2229 assert!(matches!(song.lines[1], Line::Lyrics(_)));
2230 assert!(matches!(song.lines[2], Line::Directive(_)));
2231 }
2232
2233 #[test]
2234 fn song_with_comments_and_empty_lines() {
2235 let input = "\
2236{title: Test}
2237{comment: Intro}
2238
2239[Am]Hello
2240";
2241
2242 let song = parse(input).unwrap();
2243 assert_eq!(song.lines.len(), 4);
2244 assert_eq!(
2245 song.lines[0],
2246 Line::Directive(Directive::with_value("title", "Test"))
2247 );
2248 assert_eq!(
2249 song.lines[1],
2250 Line::Comment(CommentStyle::Normal, "Intro".to_string())
2251 );
2252 assert_eq!(song.lines[2], Line::Empty);
2253 assert!(matches!(song.lines[3], Line::Lyrics(_)));
2254 }
2255
2256 #[test]
2257 fn crlf_line_endings() {
2258 let input = "{title: Test}\r\n[Am]Hello\r\n";
2259 let song = parse(input).unwrap();
2260 assert_eq!(song.lines.len(), 2);
2261 assert_eq!(
2262 song.lines[0],
2263 Line::Directive(Directive::with_value("title", "Test")),
2264 );
2265 assert!(matches!(song.lines[1], Line::Lyrics(_)));
2266 }
2267
2268 #[test]
2269 fn stray_close_brace_in_lyrics() {
2270 let result = lines("hello } world");
2272 assert_eq!(
2273 result,
2274 vec![Line::Lyrics(LyricsLine {
2275 segments: vec![LyricsSegment::text_only("hello } world")],
2276 })]
2277 );
2278 }
2279
2280 #[test]
2281 fn stray_close_bracket_in_lyrics() {
2282 let result = lines("hello ] world");
2284 assert_eq!(
2285 result,
2286 vec![Line::Lyrics(LyricsLine {
2287 segments: vec![LyricsSegment::text_only("hello ] world")],
2288 })]
2289 );
2290 }
2291
2292 #[test]
2293 fn unicode_in_chords_and_lyrics() {
2294 let result = lines("[Am]こんにちは [G]世界");
2295 assert_eq!(
2296 result,
2297 vec![Line::Lyrics(LyricsLine {
2298 segments: vec![
2299 LyricsSegment::new(Some(Chord::new("Am")), "こんにちは "),
2300 LyricsSegment::new(Some(Chord::new("G")), "世界"),
2301 ],
2302 })]
2303 );
2304 }
2305
2306 #[test]
2307 fn multiple_colons_in_directive_value() {
2308 let result = lines("{meta: key:value:extra}");
2313 assert_eq!(
2314 result,
2315 vec![Line::Directive(Directive {
2316 name: "meta".to_string(),
2317 value: None,
2318 kind: DirectiveKind::Meta("key:value:extra".to_string()),
2319 selector: None,
2320 })],
2321 );
2322
2323 let result = lines("{custom_dir: key:value:extra}");
2325 assert_eq!(
2326 result,
2327 vec![Line::Directive(Directive {
2328 name: "custom_dir".to_string(),
2329 value: Some("key:value:extra".to_string()),
2330 kind: DirectiveKind::Unknown("custom_dir".to_string()),
2331 selector: None,
2332 })],
2333 );
2334 }
2335
2336 #[test]
2337 fn directive_only_whitespace_name() {
2338 let err = parse("{ }").unwrap_err();
2339 assert!(
2340 err.message.contains("empty directive name"),
2341 "error message was: {}",
2342 err.message
2343 );
2344 }
2345
2346 #[test]
2347 fn directive_with_brackets_in_value() {
2348 let result = lines("{comment: play [Am] here}");
2350 assert_eq!(
2351 result,
2352 vec![Line::Comment(
2353 CommentStyle::Normal,
2354 "play [Am] here".to_string()
2355 )],
2356 );
2357 }
2358
2359 #[test]
2360 fn chord_line_with_spaces() {
2361 let result = lines("[Am] [G] [C]");
2362 assert_eq!(
2363 result,
2364 vec![Line::Lyrics(LyricsLine {
2365 segments: vec![
2366 LyricsSegment::new(Some(Chord::new("Am")), " "),
2367 LyricsSegment::new(Some(Chord::new("G")), " "),
2368 LyricsSegment::chord_only(Chord::new("C")),
2369 ],
2370 })]
2371 );
2372 }
2373
2374 #[test]
2375 fn trailing_newline_produces_empty_line() {
2376 let result = lines("text\n");
2377 assert_eq!(
2378 result,
2379 vec![Line::Lyrics(LyricsLine {
2380 segments: vec![LyricsSegment::text_only("text")],
2381 })]
2382 );
2383 }
2384
2385 #[test]
2386 fn parser_struct_directly() {
2387 let tokens = Lexer::new("[C]Hello").tokenize();
2389 let song = Parser::new(tokens).parse().unwrap();
2390 assert_eq!(song.lines.len(), 1);
2391 }
2392
2393 #[test]
2396 fn full_song_with_all_directive_types() {
2397 let input = "\
2398{t: Amazing Grace}
2399{st: A Hymn}
2400{artist: John Newton}
2401{key: G}
2402{tempo: 80}
2403{time: 3/4}
2404{capo: 2}
2405{comment: Verse 1}
2406{ci: Play softly}
2407{cb: Key change ahead}
2408{soc}
2409[G]Amazing [G7]grace
2410{eoc}";
2411
2412 let song = parse(input).unwrap();
2413
2414 assert_eq!(song.metadata.title.as_deref(), Some("Amazing Grace"));
2416 assert_eq!(song.metadata.subtitles, vec!["A Hymn"]);
2417 assert_eq!(song.metadata.artists, vec!["John Newton"]);
2418 assert_eq!(song.metadata.key.as_deref(), Some("G"));
2419 assert_eq!(song.metadata.tempo.as_deref(), Some("80"));
2420 assert_eq!(song.metadata.time.as_deref(), Some("3/4"));
2421 assert_eq!(song.metadata.capo.as_deref(), Some("2"));
2422
2423 assert_eq!(song.lines.len(), 13);
2425 assert!(matches!(song.lines[0], Line::Directive(_))); assert!(matches!(song.lines[1], Line::Directive(_))); assert!(matches!(song.lines[2], Line::Directive(_))); assert!(matches!(song.lines[3], Line::Directive(_))); assert!(matches!(song.lines[4], Line::Directive(_))); assert!(matches!(song.lines[5], Line::Directive(_))); assert!(matches!(song.lines[6], Line::Directive(_))); assert_eq!(
2433 song.lines[7],
2434 Line::Comment(CommentStyle::Normal, "Verse 1".to_string())
2435 );
2436 assert_eq!(
2437 song.lines[8],
2438 Line::Comment(CommentStyle::Italic, "Play softly".to_string())
2439 );
2440 assert_eq!(
2441 song.lines[9],
2442 Line::Comment(CommentStyle::Boxed, "Key change ahead".to_string())
2443 );
2444 if let Line::Directive(ref d) = song.lines[10] {
2446 assert_eq!(d.kind, DirectiveKind::StartOfChorus);
2447 assert_eq!(d.name, "start_of_chorus");
2448 } else {
2449 panic!("expected directive");
2450 }
2451 assert!(matches!(song.lines[11], Line::Lyrics(_))); if let Line::Directive(ref d) = song.lines[12] {
2454 assert_eq!(d.kind, DirectiveKind::EndOfChorus);
2455 assert_eq!(d.name, "end_of_chorus");
2456 } else {
2457 panic!("expected directive");
2458 }
2459 }
2460
2461 #[test]
2464 fn parse_error_implements_std_error() {
2465 let err = parse("[Am").unwrap_err();
2466 let _: &dyn std::error::Error = &err;
2468 }
2469
2470 #[test]
2471 fn parse_error_source_is_none() {
2472 let err = parse("[Am").unwrap_err();
2473 let err_ref: &dyn std::error::Error = &err;
2474 assert!(err_ref.source().is_none());
2475 }
2476
2477 #[test]
2478 fn parse_error_line_column_accessors() {
2479 let err = parse("[Am").unwrap_err();
2480 assert_eq!(err.line(), 1);
2481 assert_eq!(err.column(), 1);
2482 }
2483
2484 #[test]
2485 fn unclosed_chord_error_location() {
2486 let err = parse("[Am").unwrap_err();
2487 assert!(err.message.contains("unclosed chord"));
2488 assert_eq!(err.span.start.line, 1);
2489 assert_eq!(err.span.start.column, 1);
2490 }
2491
2492 #[test]
2493 fn unclosed_chord_on_second_line() {
2494 let err = parse("Hello\n[Am").unwrap_err();
2495 assert!(err.message.contains("unclosed chord"));
2496 assert_eq!(err.span.start.line, 2);
2497 assert_eq!(err.span.start.column, 1);
2498 }
2499
2500 #[test]
2501 fn unclosed_chord_mid_line() {
2502 let err = parse("text [Am").unwrap_err();
2503 assert!(err.message.contains("unclosed chord"));
2504 assert_eq!(err.span.start.line, 1);
2505 assert_eq!(err.span.start.column, 6);
2506 }
2507
2508 #[test]
2509 fn unclosed_directive_error_location() {
2510 let err = parse("{title: oops").unwrap_err();
2511 assert!(err.message.contains("unclosed directive"));
2512 assert_eq!(err.span.start.line, 1);
2514 assert_eq!(err.span.start.column, 13);
2515 }
2516
2517 #[test]
2518 fn unclosed_directive_on_third_line() {
2519 let err = parse("line one\nline two\n{title: oops").unwrap_err();
2520 assert!(err.message.contains("unclosed directive"));
2521 assert_eq!(err.span.start.line, 3);
2523 assert_eq!(err.span.start.column, 13);
2524 }
2525
2526 #[test]
2527 fn empty_directive_error_location() {
2528 let err = parse("{}").unwrap_err();
2529 assert!(err.message.contains("empty directive name"));
2530 assert_eq!(err.span.start.line, 1);
2531 assert_eq!(err.span.start.column, 1);
2532 }
2533
2534 #[test]
2535 fn empty_directive_with_colon_error_location() {
2536 let err = parse("{: value}").unwrap_err();
2537 assert!(err.message.contains("empty directive name"));
2538 assert_eq!(err.span.start.line, 1);
2539 assert_eq!(err.span.start.column, 1);
2540 }
2541
2542 #[test]
2543 fn error_display_format_with_line_column() {
2544 let err = parse("first line\n{title: no close").unwrap_err();
2545 let msg = format!("{err}");
2546 assert!(
2548 msg.starts_with("parse error at line 2, column 17:"),
2549 "unexpected display format: {msg}"
2550 );
2551 }
2552
2553 #[test]
2554 fn unclosed_chord_at_end_of_line_error_location() {
2555 let err = parse("[Am\nmore text").unwrap_err();
2557 assert!(err.message.contains("unclosed chord"));
2558 assert_eq!(err.span.start.line, 1);
2559 assert_eq!(err.span.start.column, 1);
2560 }
2561
2562 #[test]
2563 fn unclosed_directive_at_eof_error_location() {
2564 let err = parse("{title").unwrap_err();
2565 assert!(err.message.contains("unclosed directive"));
2566 assert_eq!(err.span.start.line, 1);
2567 assert_eq!(err.span.start.column, 1);
2568 }
2569
2570 #[test]
2571 fn whitespace_only_directive_name_error_location() {
2572 let err = parse("{ : value}").unwrap_err();
2573 assert!(err.message.contains("empty directive name"));
2574 assert_eq!(err.span.start.line, 1);
2575 assert_eq!(err.span.start.column, 1);
2576 }
2577
2578 #[test]
2579 fn error_after_valid_content() {
2580 let input = "{title: Test}\n[Am]Hello\n[G";
2582 let err = parse(input).unwrap_err();
2583 assert!(err.message.contains("unclosed chord"));
2584 assert_eq!(err.span.start.line, 3);
2585 assert_eq!(err.span.start.column, 1);
2586 }
2587
2588 #[test]
2589 fn multiple_errors_first_is_reported() {
2590 let err = parse("{title\n{another").unwrap_err();
2592 assert!(err.message.contains("unclosed directive"));
2593 assert_eq!(err.span.start.line, 1);
2594 }
2595
2596 #[test]
2599 fn tab_content_is_verbatim() {
2600 let song = parse("{start_of_tab}\ne|---[0]---|\n{end_of_tab}").unwrap();
2602 if let Line::Lyrics(ref l) = song.lines[1] {
2606 assert_eq!(l.segments.len(), 1);
2607 assert!(l.segments[0].chord.is_none());
2608 assert_eq!(l.segments[0].text, "e|---[0]---|");
2609 } else {
2610 panic!("expected lyrics line for tab content");
2611 }
2612 }
2613
2614 #[test]
2615 fn tab_content_preserves_braces() {
2616 let song = parse("{sot}\n{some text}\n{eot}").unwrap();
2617 if let Line::Lyrics(ref l) = song.lines[1] {
2618 assert_eq!(l.segments[0].text, "{some text}");
2619 } else {
2620 panic!("expected lyrics line for tab content");
2621 }
2622 }
2623
2624 #[test]
2625 fn chords_parsed_after_tab_ends() {
2626 let song = parse("{sot}\ne|---|\n{eot}\n[Am]Hello").unwrap();
2628 if let Line::Lyrics(ref l) = song.lines[3] {
2630 assert!(l.segments[0].chord.is_some());
2631 assert_eq!(l.segments[0].chord.as_ref().unwrap().name, "Am");
2632 } else {
2633 panic!("expected lyrics line with chord after tab section");
2634 }
2635 }
2636
2637 #[test]
2640 fn grid_content_is_verbatim() {
2641 let song = parse("{start_of_grid}\n| [Am] . | [C] . |\n{end_of_grid}").unwrap();
2643 if let Line::Lyrics(ref l) = song.lines[1] {
2647 assert_eq!(l.segments.len(), 1);
2648 assert!(l.segments[0].chord.is_none());
2649 assert_eq!(l.segments[0].text, "| [Am] . | [C] . |");
2650 } else {
2651 panic!("expected lyrics line for grid content");
2652 }
2653 }
2654
2655 #[test]
2656 fn grid_content_preserves_braces() {
2657 let song = parse("{sog}\n{some text}\n{eog}").unwrap();
2658 if let Line::Lyrics(ref l) = song.lines[1] {
2659 assert_eq!(l.segments[0].text, "{some text}");
2660 } else {
2661 panic!("expected lyrics line for grid content");
2662 }
2663 }
2664
2665 #[test]
2666 fn chords_parsed_after_grid_ends() {
2667 let song = parse("{sog}\n| Am . |\n{eog}\n[Am]Hello").unwrap();
2669 if let Line::Lyrics(ref l) = song.lines[3] {
2671 assert!(l.segments[0].chord.is_some());
2672 assert_eq!(l.segments[0].chord.as_ref().unwrap().name, "Am");
2673 } else {
2674 panic!("expected lyrics line with chord after grid section");
2675 }
2676 }
2677
2678 #[test]
2679 fn grid_short_aliases_sog_eog() {
2680 let song = parse("{sog}\n| Am |\n{eog}").unwrap();
2681 if let Line::Directive(ref d) = song.lines[0] {
2682 assert_eq!(d.kind, DirectiveKind::StartOfGrid);
2683 assert_eq!(d.name, "start_of_grid");
2684 } else {
2685 panic!("expected start_of_grid directive");
2686 }
2687 if let Line::Directive(ref d) = song.lines[2] {
2688 assert_eq!(d.kind, DirectiveKind::EndOfGrid);
2689 assert_eq!(d.name, "end_of_grid");
2690 } else {
2691 panic!("expected end_of_grid directive");
2692 }
2693 }
2694
2695 #[test]
2696 fn grid_with_label() {
2697 let song = parse("{start_of_grid: Intro}\n| Am . | C . |\n{end_of_grid}").unwrap();
2698 if let Line::Directive(ref d) = song.lines[0] {
2699 assert_eq!(d.kind, DirectiveKind::StartOfGrid);
2700 assert_eq!(d.value.as_deref(), Some("Intro"));
2701 } else {
2702 panic!("expected start_of_grid directive with label");
2703 }
2704 }
2705
2706 #[test]
2709 fn define_directive_parsed() {
2710 let song = parse("{define: Asus4 base-fret 1 frets x 0 2 2 3 0}").unwrap();
2711 if let Line::Directive(ref d) = song.lines[0] {
2712 assert_eq!(d.kind, DirectiveKind::Define);
2713 assert_eq!(d.name, "define");
2714 assert_eq!(
2715 d.value.as_deref(),
2716 Some("Asus4 base-fret 1 frets x 0 2 2 3 0")
2717 );
2718 } else {
2719 panic!("expected define directive");
2720 }
2721 }
2722
2723 #[test]
2724 fn chord_directive_parsed() {
2725 let song = parse("{chord: Asus4}").unwrap();
2726 if let Line::Directive(ref d) = song.lines[0] {
2727 assert_eq!(d.kind, DirectiveKind::ChordDirective);
2728 assert_eq!(d.value.as_deref(), Some("Asus4"));
2729 } else {
2730 panic!("expected chord directive");
2731 }
2732 }
2733
2734 #[test]
2735 fn page_control_directives_long_form() {
2736 let song = parse("{new_page}\n{new_physical_page}\n{column_break}\n{columns: 2}").unwrap();
2737 if let Line::Directive(ref d) = song.lines[0] {
2738 assert_eq!(d.kind, DirectiveKind::NewPage);
2739 assert_eq!(d.name, "new_page");
2740 assert!(d.value.is_none());
2741 } else {
2742 panic!("expected new_page directive");
2743 }
2744 if let Line::Directive(ref d) = song.lines[1] {
2745 assert_eq!(d.kind, DirectiveKind::NewPhysicalPage);
2746 assert_eq!(d.name, "new_physical_page");
2747 assert!(d.value.is_none());
2748 } else {
2749 panic!("expected new_physical_page directive");
2750 }
2751 if let Line::Directive(ref d) = song.lines[2] {
2752 assert_eq!(d.kind, DirectiveKind::ColumnBreak);
2753 assert_eq!(d.name, "column_break");
2754 assert!(d.value.is_none());
2755 } else {
2756 panic!("expected column_break directive");
2757 }
2758 if let Line::Directive(ref d) = song.lines[3] {
2759 assert_eq!(d.kind, DirectiveKind::Columns);
2760 assert_eq!(d.name, "columns");
2761 assert_eq!(d.value.as_deref(), Some("2"));
2762 } else {
2763 panic!("expected columns directive");
2764 }
2765 }
2766
2767 #[test]
2768 fn page_control_directives_short_form() {
2769 let song = parse("{np}\n{npp}\n{colb}\n{col: 3}").unwrap();
2770 if let Line::Directive(ref d) = song.lines[0] {
2771 assert_eq!(d.kind, DirectiveKind::NewPage);
2772 assert_eq!(d.name, "new_page");
2773 } else {
2774 panic!("expected new_page directive");
2775 }
2776 if let Line::Directive(ref d) = song.lines[1] {
2777 assert_eq!(d.kind, DirectiveKind::NewPhysicalPage);
2778 assert_eq!(d.name, "new_physical_page");
2779 } else {
2780 panic!("expected new_physical_page directive");
2781 }
2782 if let Line::Directive(ref d) = song.lines[2] {
2783 assert_eq!(d.kind, DirectiveKind::ColumnBreak);
2784 assert_eq!(d.name, "column_break");
2785 } else {
2786 panic!("expected column_break directive");
2787 }
2788 if let Line::Directive(ref d) = song.lines[3] {
2789 assert_eq!(d.kind, DirectiveKind::Columns);
2790 assert_eq!(d.name, "columns");
2791 assert_eq!(d.value.as_deref(), Some("3"));
2792 } else {
2793 panic!("expected columns directive");
2794 }
2795 }
2796
2797 #[test]
2798 fn page_control_not_metadata() {
2799 let song = parse("{new_page}\n{columns: 2}").unwrap();
2800 assert!(song.metadata.title.is_none());
2802 assert!(song.metadata.custom.is_empty());
2803 }
2804
2805 #[test]
2808 fn parse_lenient_no_errors() {
2809 let result = parse_lenient("{title: Test}\n[Am]Hello");
2810 assert!(result.is_ok());
2811 assert!(!result.has_errors());
2812 assert_eq!(result.song.metadata.title.as_deref(), Some("Test"));
2813 assert_eq!(result.song.lines.len(), 2);
2814 }
2815
2816 #[test]
2817 fn parse_lenient_collects_multiple_errors() {
2818 let result = parse_lenient("{title\nHello world\n[Am");
2820 assert!(result.has_errors());
2821 assert_eq!(result.errors.len(), 2);
2822 assert!(result.song.lines.iter().any(|l| {
2824 if let Line::Lyrics(ll) = l {
2825 ll.text() == "Hello world"
2826 } else {
2827 false
2828 }
2829 }));
2830 }
2831
2832 #[test]
2833 fn parse_lenient_partial_ast_with_metadata() {
2834 let result = parse_lenient("{title: My Song}\n{bad\n[G]La la");
2836 assert_eq!(result.errors.len(), 1);
2837 assert_eq!(result.song.metadata.title.as_deref(), Some("My Song"));
2838 assert!(result.song.lines.len() >= 2);
2840 }
2841
2842 #[test]
2843 fn parse_lenient_all_lines_bad() {
2844 let result = parse_lenient("{unclosed\n[bad");
2845 assert_eq!(result.errors.len(), 2);
2846 assert!(result.song.lines.is_empty());
2847 }
2848
2849 #[test]
2850 fn parse_lenient_error_locations() {
2851 let result = parse_lenient("{ok: fine}\n{bad\n[Am]Good\n{also bad");
2852 assert_eq!(result.errors.len(), 2);
2853 assert_eq!(result.errors[0].line(), 2);
2854 assert_eq!(result.errors[1].line(), 4);
2855 }
2856
2857 #[test]
2858 fn parse_lenient_empty_input() {
2859 let result = parse_lenient("");
2860 assert!(result.is_ok());
2861 assert!(result.song.lines.is_empty());
2862 }
2863
2864 #[test]
2865 fn parse_lenient_size_limit() {
2866 let opts = ParseOptions {
2867 max_input_size: 10,
2868 ..Default::default()
2869 };
2870 let result = parse_lenient_with_options("this input is too long", &opts);
2871 assert!(result.has_errors());
2872 assert_eq!(result.errors.len(), 1);
2873 assert!(result.errors[0].message.contains("exceeds maximum"));
2874 }
2875
2876 #[test]
2877 fn parse_lenient_max_errors_limits_collection() {
2878 let input: String = (0..100).map(|_| "{unclosed\n").collect();
2880 let opts = ParseOptions {
2881 max_errors: 5,
2882 ..Default::default()
2883 };
2884 let result = parse_lenient_with_options(&input, &opts);
2885 assert!(result.has_errors());
2886 assert_eq!(result.errors.len(), 5);
2887 }
2888
2889 #[test]
2890 fn parse_lenient_zero_max_errors_disables_limit() {
2891 let input: String = (0..20).map(|_| "{unclosed\n").collect();
2892 let opts = ParseOptions {
2893 max_errors: 0,
2894 ..Default::default()
2895 };
2896 let result = parse_lenient_with_options(&input, &opts);
2897 assert_eq!(result.errors.len(), 20);
2898 }
2899
2900 #[test]
2901 fn transpose_directive_parsed() {
2902 let song = parse("{transpose: 2}").expect("parse failed");
2903 assert_eq!(song.lines.len(), 1);
2904 if let Line::Directive(ref d) = song.lines[0] {
2905 assert_eq!(d.kind, DirectiveKind::Transpose);
2906 assert_eq!(d.name, "transpose");
2907 assert_eq!(d.value.as_deref(), Some("2"));
2908 } else {
2909 panic!("expected transpose directive");
2910 }
2911 }
2912
2913 #[test]
2914 fn transpose_directive_negative_value() {
2915 let song = parse("{transpose: -3}").expect("parse failed");
2916 if let Line::Directive(ref d) = song.lines[0] {
2917 assert_eq!(d.kind, DirectiveKind::Transpose);
2918 assert_eq!(d.value.as_deref(), Some("-3"));
2919 } else {
2920 panic!("expected transpose directive");
2921 }
2922 }
2923
2924 #[test]
2925 fn transpose_directive_no_value() {
2926 let song = parse("{transpose}").expect("parse failed");
2927 if let Line::Directive(ref d) = song.lines[0] {
2928 assert_eq!(d.kind, DirectiveKind::Transpose);
2929 assert!(d.value.is_none());
2930 } else {
2931 panic!("expected transpose directive");
2932 }
2933 }
2934
2935 #[test]
2936 fn transpose_directive_is_not_metadata() {
2937 let kind = DirectiveKind::Transpose;
2938 assert!(!kind.is_metadata());
2939 }
2940
2941 #[test]
2942 fn transpose_directive_case_insensitive() {
2943 let song = parse("{Transpose: 5}").expect("parse failed");
2944 if let Line::Directive(ref d) = song.lines[0] {
2945 assert_eq!(d.kind, DirectiveKind::Transpose);
2946 assert_eq!(d.name, "transpose");
2947 assert_eq!(d.value.as_deref(), Some("5"));
2948 } else {
2949 panic!("expected transpose directive");
2950 }
2951 }
2952
2953 #[test]
2956 fn custom_section_start_parsed() {
2957 let result = lines("{start_of_intro}");
2958 if let Line::Directive(ref d) = result[0] {
2959 assert_eq!(d.name, "start_of_intro");
2960 assert_eq!(d.kind, DirectiveKind::StartOfSection("intro".to_string()));
2961 assert!(d.is_section_start());
2962 } else {
2963 panic!("expected directive");
2964 }
2965 }
2966
2967 #[test]
2968 fn custom_section_end_parsed() {
2969 let result = lines("{end_of_intro}");
2970 if let Line::Directive(ref d) = result[0] {
2971 assert_eq!(d.name, "end_of_intro");
2972 assert_eq!(d.kind, DirectiveKind::EndOfSection("intro".to_string()));
2973 assert!(d.is_section_end());
2974 } else {
2975 panic!("expected directive");
2976 }
2977 }
2978
2979 #[test]
2980 fn custom_section_with_label() {
2981 let result = lines("{start_of_intro: Guitar Intro}");
2982 if let Line::Directive(ref d) = result[0] {
2983 assert_eq!(d.name, "start_of_intro");
2984 assert_eq!(d.value.as_deref(), Some("Guitar Intro"));
2985 assert_eq!(d.kind, DirectiveKind::StartOfSection("intro".to_string()));
2986 } else {
2987 panic!("expected directive");
2988 }
2989 }
2990
2991 #[test]
2992 fn custom_section_lyrics_parsed_normally() {
2993 let song = parse("{start_of_intro}\n[Am]Hello [G]world\n{end_of_intro}").unwrap();
2994 assert_eq!(song.lines.len(), 3);
2996 if let Line::Lyrics(ref l) = song.lines[1] {
2997 assert!(l.has_chords());
2998 assert_eq!(l.segments.len(), 2);
2999 assert_eq!(l.segments[0].chord.as_ref().unwrap().name, "Am");
3000 } else {
3001 panic!("expected lyrics line inside custom section");
3002 }
3003 }
3004
3005 #[test]
3006 fn custom_section_various_names() {
3007 for name in &["outro", "solo", "interlude", "coda", "pre_chorus"] {
3008 let input = format!("{{start_of_{name}}}");
3009 let result = lines(&input);
3010 if let Line::Directive(ref d) = result[0] {
3011 assert_eq!(d.name, format!("start_of_{name}"));
3012 assert!(d.is_section_start(), "should be section start for {name}");
3013 } else {
3014 panic!("expected directive for {name}");
3015 }
3016 }
3017 }
3018
3019 #[test]
3022 fn lyrics_with_bold_markup() {
3023 use crate::inline_markup::TextSpan;
3024
3025 let result = lines("[Am]Hello <b>world</b>");
3026 match &result[0] {
3027 Line::Lyrics(lyrics) => {
3028 assert_eq!(lyrics.segments.len(), 1);
3029 let seg = &lyrics.segments[0];
3030 assert_eq!(seg.text, "Hello world");
3031 assert_eq!(
3032 seg.spans,
3033 vec![
3034 TextSpan::Plain("Hello ".to_string()),
3035 TextSpan::Bold(vec![TextSpan::Plain("world".to_string())]),
3036 ]
3037 );
3038 }
3039 _ => panic!("expected lyrics line"),
3040 }
3041 }
3042
3043 #[test]
3044 fn custom_section_case_insensitive() {
3045 let result = lines("{Start_Of_Intro}");
3046 if let Line::Directive(ref d) = result[0] {
3047 assert_eq!(d.name, "start_of_intro");
3048 assert_eq!(d.kind, DirectiveKind::StartOfSection("intro".to_string()));
3049 } else {
3050 panic!("expected directive");
3051 }
3052 }
3053
3054 #[test]
3057 fn image_directive_basic() {
3058 let song = parse("{image: src=photo.jpg}").unwrap();
3059 if let Line::Directive(ref d) = song.lines[0] {
3060 assert_eq!(d.name, "image");
3061 if let DirectiveKind::Image(ref attrs) = d.kind {
3062 assert_eq!(attrs.src, "photo.jpg");
3063 assert!(attrs.width.is_none());
3064 assert!(attrs.height.is_none());
3065 assert!(attrs.scale.is_none());
3066 assert!(attrs.title.is_none());
3067 assert!(attrs.anchor.is_none());
3068 } else {
3069 panic!("expected Image directive kind");
3070 }
3071 } else {
3072 panic!("expected directive");
3073 }
3074 }
3075
3076 #[test]
3079 fn lyrics_without_markup_has_empty_spans() {
3080 let result = lines("[Am]Hello world");
3081 match &result[0] {
3082 Line::Lyrics(lyrics) => {
3083 assert_eq!(lyrics.segments[0].text, "Hello world");
3084 assert!(lyrics.segments[0].spans.is_empty());
3085 }
3086 _ => panic!("expected lyrics line"),
3087 }
3088 }
3089
3090 #[test]
3091 fn image_directive_all_attributes() {
3092 let song =
3093 parse(r#"{image: src=logo.png width=200 height=100 scale=0.5 title="Album Cover" anchor=top}"#)
3094 .unwrap();
3095 if let Line::Directive(ref d) = song.lines[0] {
3096 if let DirectiveKind::Image(ref attrs) = d.kind {
3097 assert_eq!(attrs.src, "logo.png");
3098 assert_eq!(attrs.width.as_deref(), Some("200"));
3099 assert_eq!(attrs.height.as_deref(), Some("100"));
3100 assert_eq!(attrs.scale.as_deref(), Some("0.5"));
3101 assert_eq!(attrs.title.as_deref(), Some("Album Cover"));
3102 assert_eq!(attrs.anchor.as_deref(), Some("top"));
3103 } else {
3104 panic!("expected Image directive kind");
3105 }
3106 } else {
3107 panic!("expected directive");
3108 }
3109 }
3110
3111 #[test]
3112 fn lyrics_with_nested_markup() {
3113 use crate::inline_markup::TextSpan;
3114
3115 let result = lines("<b><i>both</i></b>");
3116 match &result[0] {
3117 Line::Lyrics(lyrics) => {
3118 assert_eq!(lyrics.segments[0].text, "both");
3119 assert_eq!(
3120 lyrics.segments[0].spans,
3121 vec![TextSpan::Bold(vec![TextSpan::Italic(vec![
3122 TextSpan::Plain("both".to_string())
3123 ])])]
3124 );
3125 }
3126 _ => panic!("expected lyrics line"),
3127 }
3128 }
3129
3130 #[test]
3131 fn image_directive_quoted_value_with_spaces() {
3132 let song = parse(r#"{image: src=cover.jpg title="My Great Album"}"#).unwrap();
3133 if let Line::Directive(ref d) = song.lines[0] {
3134 if let DirectiveKind::Image(ref attrs) = d.kind {
3135 assert_eq!(attrs.src, "cover.jpg");
3136 assert_eq!(attrs.title.as_deref(), Some("My Great Album"));
3137 } else {
3138 panic!("expected Image directive kind");
3139 }
3140 } else {
3141 panic!("expected directive");
3142 }
3143 }
3144
3145 #[test]
3146 fn lyrics_markup_text_field_has_stripped_content() {
3147 let result = lines("<b>bold</b> and <i>italic</i> text");
3148 match &result[0] {
3149 Line::Lyrics(lyrics) => {
3150 assert_eq!(lyrics.segments[0].text, "bold and italic text");
3152 assert!(!lyrics.segments[0].spans.is_empty());
3154 }
3155 _ => panic!("expected lyrics line"),
3156 }
3157 }
3158
3159 #[test]
3160 fn image_directive_no_value() {
3161 let song = parse("{image}").unwrap();
3162 if let Line::Directive(ref d) = song.lines[0] {
3163 assert_eq!(d.name, "image");
3164 if let DirectiveKind::Image(ref attrs) = d.kind {
3165 assert_eq!(attrs.src, "");
3166 } else {
3167 panic!("expected Image directive kind");
3168 }
3169 } else {
3170 panic!("expected directive");
3171 }
3172 }
3173
3174 #[test]
3175 fn image_directive_unknown_attributes_ignored() {
3176 let song = parse("{image: src=pic.jpg unknown=foo bar}").unwrap();
3177 if let Line::Directive(ref d) = song.lines[0] {
3178 if let DirectiveKind::Image(ref attrs) = d.kind {
3179 assert_eq!(attrs.src, "pic.jpg");
3180 } else {
3182 panic!("expected Image directive kind");
3183 }
3184 } else {
3185 panic!("expected directive");
3186 }
3187 }
3188
3189 #[test]
3190 fn image_directive_case_insensitive() {
3191 let song = parse("{IMAGE: src=photo.jpg}").unwrap();
3192 if let Line::Directive(ref d) = song.lines[0] {
3193 assert_eq!(d.name, "image");
3194 assert!(d.kind.is_image());
3195 } else {
3196 panic!("expected directive");
3197 }
3198 }
3199
3200 #[test]
3201 fn image_directive_width_only() {
3202 let song = parse("{image: src=img.png width=50%}").unwrap();
3203 if let Line::Directive(ref d) = song.lines[0] {
3204 if let DirectiveKind::Image(ref attrs) = d.kind {
3205 assert_eq!(attrs.src, "img.png");
3206 assert_eq!(attrs.width.as_deref(), Some("50%"));
3207 assert!(attrs.height.is_none());
3208 } else {
3209 panic!("expected Image directive kind");
3210 }
3211 } else {
3212 panic!("expected directive");
3213 }
3214 }
3215
3216 #[test]
3217 fn image_directive_preserves_raw_value() {
3218 let song = parse("{image: src=photo.jpg width=200}").unwrap();
3219 if let Line::Directive(ref d) = song.lines[0] {
3220 assert_eq!(d.value.as_deref(), Some("src=photo.jpg width=200"));
3222 } else {
3223 panic!("expected directive");
3224 }
3225 }
3226
3227 #[test]
3230 fn parse_image_attributes_empty_input() {
3231 let attrs = super::parse_image_attributes("");
3232 assert_eq!(attrs.src, "");
3233 assert!(attrs.width.is_none());
3234 }
3235
3236 #[test]
3237 fn parse_image_attributes_src_only() {
3238 let attrs = super::parse_image_attributes("src=test.png");
3239 assert_eq!(attrs.src, "test.png");
3240 }
3241
3242 #[test]
3243 fn parse_image_attributes_multiple() {
3244 let attrs = super::parse_image_attributes("src=a.jpg width=100 height=200");
3245 assert_eq!(attrs.src, "a.jpg");
3246 assert_eq!(attrs.width.as_deref(), Some("100"));
3247 assert_eq!(attrs.height.as_deref(), Some("200"));
3248 }
3249
3250 #[test]
3251 fn parse_image_attributes_quoted_value() {
3252 let attrs = super::parse_image_attributes(r#"src=a.jpg title="Hello World""#);
3253 assert_eq!(attrs.src, "a.jpg");
3254 assert_eq!(attrs.title.as_deref(), Some("Hello World"));
3255 }
3256
3257 #[test]
3258 fn parse_image_attributes_extra_whitespace() {
3259 let attrs = super::parse_image_attributes(" src=a.jpg width=100 ");
3260 assert_eq!(attrs.src, "a.jpg");
3261 assert_eq!(attrs.width.as_deref(), Some("100"));
3262 }
3263
3264 #[test]
3265 fn parse_image_attributes_case_insensitive_keys() {
3266 let attrs = super::parse_image_attributes("SRC=photo.jpg WIDTH=200 Height=100");
3267 assert_eq!(attrs.src, "photo.jpg");
3268 assert_eq!(attrs.width.as_deref(), Some("200"));
3269 assert_eq!(attrs.height.as_deref(), Some("100"));
3270 }
3271
3272 #[test]
3273 fn parse_image_attributes_mixed_case_keys() {
3274 let attrs = super::parse_image_attributes("Src=a.jpg Scale=0.5 Title=test Anchor=column");
3275 assert_eq!(attrs.src, "a.jpg");
3276 assert_eq!(attrs.scale.as_deref(), Some("0.5"));
3277 assert_eq!(attrs.title.as_deref(), Some("test"));
3278 assert_eq!(attrs.anchor.as_deref(), Some("column"));
3279 }
3280
3281 #[test]
3282 fn parse_image_attributes_src_truncated_at_limit() {
3283 let long_src = "a".repeat(5000);
3284 let input = format!("src={long_src}");
3285 let attrs = super::parse_image_attributes(&input);
3286 assert_eq!(attrs.src.len(), super::IMAGE_SRC_MAX_BYTES);
3287 }
3288
3289 #[test]
3290 fn parse_image_attributes_other_attrs_truncated_at_limit() {
3291 let long_title = "x".repeat(2000);
3292 let input = format!("src=ok.jpg title=\"{long_title}\" width={long_title}");
3293 let attrs = super::parse_image_attributes(&input);
3294 assert_eq!(attrs.src, "ok.jpg");
3295 assert_eq!(
3296 attrs.title.as_deref().map(str::len),
3297 Some(super::IMAGE_ATTR_MAX_BYTES)
3298 );
3299 assert_eq!(
3300 attrs.width.as_deref().map(str::len),
3301 Some(super::IMAGE_ATTR_MAX_BYTES)
3302 );
3303 }
3304
3305 #[test]
3306 fn parse_image_attributes_truncation_respects_utf8_boundary() {
3307 let cjk = "漢".repeat(342); let input = format!("title=\"{cjk}\"");
3311 let attrs = super::parse_image_attributes(&input);
3312 let title = attrs.title.unwrap();
3313 assert!(title.len() <= super::IMAGE_ATTR_MAX_BYTES);
3314 assert_eq!(title.len(), 1023); }
3318
3319 #[test]
3320 fn parse_image_attributes_values_within_limit_unchanged() {
3321 let title = "a".repeat(1024);
3322 let input = format!("src=ok.jpg title=\"{title}\"");
3323 let attrs = super::parse_image_attributes(&input);
3324 assert_eq!(attrs.title.as_deref(), Some(title.as_str()));
3325 }
3326
3327 #[test]
3328 fn truncate_string_empty() {
3329 assert_eq!(super::truncate_string(String::new(), 100), "");
3330 }
3331
3332 #[test]
3333 fn split_key_value_pairs_basic() {
3334 let pairs = super::split_key_value_pairs("key=value");
3335 assert_eq!(pairs, vec![("key".to_string(), "value".to_string())]);
3336 }
3337
3338 #[test]
3339 fn split_key_value_pairs_quoted() {
3340 let pairs = super::split_key_value_pairs(r#"key="hello world""#);
3341 assert_eq!(pairs, vec![("key".to_string(), "hello world".to_string())]);
3342 }
3343
3344 #[test]
3345 fn split_key_value_pairs_mixed() {
3346 let pairs = super::split_key_value_pairs(r#"a=1 b="two three" c=4"#);
3347 assert_eq!(pairs.len(), 3);
3348 assert_eq!(pairs[0], ("a".to_string(), "1".to_string()));
3349 assert_eq!(pairs[1], ("b".to_string(), "two three".to_string()));
3350 assert_eq!(pairs[2], ("c".to_string(), "4".to_string()));
3351 }
3352
3353 #[test]
3354 fn split_key_value_pairs_no_equals() {
3355 let pairs = super::split_key_value_pairs("bare_token");
3356 assert!(pairs.is_empty());
3357 }
3358
3359 #[test]
3360 fn split_key_value_pairs_empty() {
3361 let pairs = super::split_key_value_pairs("");
3362 assert!(pairs.is_empty());
3363 }
3364
3365 #[test]
3366 fn split_key_value_pairs_unterminated_quote() {
3367 let pairs = super::split_key_value_pairs(r#"key="hello world"#);
3369 assert_eq!(pairs, vec![("key".to_string(), "hello world".to_string())]);
3370 }
3371
3372 #[test]
3373 fn parse_image_attributes_unterminated_quoted_title() {
3374 let attrs = super::parse_image_attributes(r#"src=photo.jpg title="My Album"#);
3377 assert_eq!(attrs.src, "photo.jpg");
3378 assert_eq!(attrs.title.as_deref(), Some("My Album"));
3379 }
3380
3381 #[test]
3382 fn parse_image_attributes_unterminated_quote_with_trailing_attrs() {
3383 let attrs = super::parse_image_attributes(r#"src=photo.jpg title="My Album width=100"#);
3385 assert_eq!(attrs.src, "photo.jpg");
3386 assert_eq!(attrs.title.as_deref(), Some("My Album width=100"));
3387 assert!(attrs.width.is_none());
3389 }
3390}
3391
3392#[cfg(test)]
3393mod delegate_tests {
3394 use super::*;
3395 use crate::ast::{DirectiveKind, Line};
3396
3397 fn lines(input: &str) -> Vec<Line> {
3398 parse(input).expect("parse failed").lines
3399 }
3400
3401 #[test]
3402 fn abc_content_is_verbatim() {
3403 let song = parse("{start_of_abc}\nX:1\nK:G\n{end_of_abc}").unwrap();
3404 assert_eq!(song.lines.len(), 4);
3405 if let Line::Lyrics(ref l) = song.lines[1] {
3406 assert_eq!(l.segments.len(), 1);
3407 assert!(l.segments[0].chord.is_none());
3408 assert_eq!(l.segments[0].text, "X:1");
3409 } else {
3410 panic!("expected lyrics line for ABC content");
3411 }
3412 }
3413
3414 #[test]
3415 fn abc_preserves_brackets() {
3416 let song = parse("{start_of_abc}\n|:GABc|[1d2d2:|[2d4d4:|\n{end_of_abc}").unwrap();
3417 if let Line::Lyrics(ref l) = song.lines[1] {
3418 assert_eq!(l.segments[0].text, "|:GABc|[1d2d2:|[2d4d4:|");
3419 } else {
3420 panic!("expected verbatim lyrics line");
3421 }
3422 }
3423
3424 #[test]
3425 fn ly_content_is_verbatim() {
3426 let song = parse("{start_of_ly}\n\\relative c' { c4 d e f }\n{end_of_ly}").unwrap();
3427 assert_eq!(song.lines.len(), 3);
3428 if let Line::Lyrics(ref l) = song.lines[1] {
3429 assert!(l.segments[0].chord.is_none());
3430 } else {
3431 panic!("expected lyrics line for Lilypond content");
3432 }
3433 }
3434
3435 #[test]
3436 fn svg_content_is_verbatim() {
3437 let song = parse("{start_of_svg}\n<svg><rect/></svg>\n{end_of_svg}").unwrap();
3438 assert_eq!(song.lines.len(), 3);
3439 if let Line::Lyrics(ref l) = song.lines[1] {
3440 assert!(l.segments[0].chord.is_none());
3441 } else {
3442 panic!("expected lyrics line for SVG content");
3443 }
3444 }
3445
3446 #[test]
3447 fn textblock_content_is_verbatim() {
3448 let song = parse("{start_of_textblock}\n[Am]Not a chord\n{end_of_textblock}").unwrap();
3449 assert_eq!(song.lines.len(), 3);
3450 if let Line::Lyrics(ref l) = song.lines[1] {
3451 assert_eq!(l.segments.len(), 1);
3452 assert!(l.segments[0].chord.is_none());
3453 assert_eq!(l.segments[0].text, "[Am]Not a chord");
3454 } else {
3455 panic!("expected lyrics line for textblock content");
3456 }
3457 }
3458
3459 #[test]
3460 fn textblock_preserves_braces() {
3461 let song = parse("{start_of_textblock}\n{some directive}\n{end_of_textblock}").unwrap();
3462 if let Line::Lyrics(ref l) = song.lines[1] {
3463 assert_eq!(l.segments[0].text, "{some directive}");
3464 } else {
3465 panic!("expected verbatim lyrics line");
3466 }
3467 }
3468
3469 #[test]
3470 fn chords_parsed_after_abc_ends() {
3471 let song = parse("{start_of_abc}\nX:1\n{end_of_abc}\n[Am]Hello").unwrap();
3472 if let Line::Lyrics(ref l) = song.lines[3] {
3473 assert!(l.segments[0].chord.is_some());
3474 assert_eq!(l.segments[0].chord.as_ref().unwrap().name, "Am");
3475 } else {
3476 panic!("expected lyrics line with chord after ABC section");
3477 }
3478 }
3479
3480 #[test]
3481 fn chords_parsed_after_ly_ends() {
3482 let song = parse("{start_of_ly}\nnotes\n{end_of_ly}\n[G]Hello").unwrap();
3483 if let Line::Lyrics(ref l) = song.lines[3] {
3484 assert!(l.segments[0].chord.is_some());
3485 assert_eq!(l.segments[0].chord.as_ref().unwrap().name, "G");
3486 } else {
3487 panic!("expected lyrics line with chord after Lilypond section");
3488 }
3489 }
3490
3491 #[test]
3492 fn chords_parsed_after_svg_ends() {
3493 let song = parse("{start_of_svg}\n<svg/>\n{end_of_svg}\n[C]Hello").unwrap();
3494 if let Line::Lyrics(ref l) = song.lines[3] {
3495 assert!(l.segments[0].chord.is_some());
3496 assert_eq!(l.segments[0].chord.as_ref().unwrap().name, "C");
3497 } else {
3498 panic!("expected lyrics line with chord after SVG section");
3499 }
3500 }
3501
3502 #[test]
3503 fn chords_parsed_after_textblock_ends() {
3504 let song = parse("{start_of_textblock}\ntext\n{end_of_textblock}\n[D]Hello").unwrap();
3505 if let Line::Lyrics(ref l) = song.lines[3] {
3506 assert!(l.segments[0].chord.is_some());
3507 assert_eq!(l.segments[0].chord.as_ref().unwrap().name, "D");
3508 } else {
3509 panic!("expected lyrics line with chord after textblock section");
3510 }
3511 }
3512
3513 #[test]
3514 fn abc_directive_with_label() {
3515 let result = lines("{start_of_abc: My Melody}");
3516 if let Line::Directive(ref d) = result[0] {
3517 assert_eq!(d.kind, DirectiveKind::StartOfAbc);
3518 assert_eq!(d.value.as_deref(), Some("My Melody"));
3519 } else {
3520 panic!("expected directive");
3521 }
3522 }
3523
3524 #[test]
3527 fn selector_suffix_on_metadata_directive() {
3528 let result = lines("{title-piano: My Song}");
3529 if let Line::Directive(ref d) = result[0] {
3530 assert_eq!(d.name, "title");
3531 assert_eq!(d.value.as_deref(), Some("My Song"));
3532 assert_eq!(d.kind, DirectiveKind::Title);
3533 assert_eq!(d.selector.as_deref(), Some("piano"));
3534 } else {
3535 panic!("expected directive");
3536 }
3537 }
3538
3539 #[test]
3540 fn textblock_directive_with_label() {
3541 let result = lines("{start_of_textblock: Notes}");
3542 if let Line::Directive(ref d) = result[0] {
3543 assert_eq!(d.kind, DirectiveKind::StartOfTextblock);
3544 assert_eq!(d.value.as_deref(), Some("Notes"));
3545 } else {
3546 panic!("expected directive");
3547 }
3548 }
3549
3550 #[test]
3551 fn selector_suffix_on_key_directive() {
3552 let result = lines("{key-bass: E}");
3553 if let Line::Directive(ref d) = result[0] {
3554 assert_eq!(d.name, "key");
3555 assert_eq!(d.value.as_deref(), Some("E"));
3556 assert_eq!(d.kind, DirectiveKind::Key);
3557 assert_eq!(d.selector.as_deref(), Some("bass"));
3558 } else {
3559 panic!("expected directive");
3560 }
3561 }
3562
3563 #[test]
3564 fn delegate_sections_not_custom() {
3565 assert_eq!(
3566 DirectiveKind::from_name("start_of_abc"),
3567 DirectiveKind::StartOfAbc
3568 );
3569 assert_eq!(
3570 DirectiveKind::from_name("start_of_ly"),
3571 DirectiveKind::StartOfLy
3572 );
3573 assert_eq!(
3574 DirectiveKind::from_name("start_of_svg"),
3575 DirectiveKind::StartOfSvg
3576 );
3577 assert_eq!(
3578 DirectiveKind::from_name("start_of_textblock"),
3579 DirectiveKind::StartOfTextblock
3580 );
3581 }
3582
3583 #[test]
3584 fn lyrics_markup_preserves_backward_compat() {
3585 let result = lines("[Am]Hello <b>bold</b> [G]world");
3587 match &result[0] {
3588 Line::Lyrics(lyrics) => {
3589 assert_eq!(lyrics.text(), "Hello bold world");
3590 }
3591 _ => panic!("expected lyrics line"),
3592 }
3593 }
3594
3595 #[test]
3598 fn new_song_directive_kind() {
3599 assert_eq!(DirectiveKind::from_name("new_song"), DirectiveKind::NewSong);
3600 assert_eq!(DirectiveKind::from_name("ns"), DirectiveKind::NewSong);
3601 assert_eq!(DirectiveKind::from_name("NEW_SONG"), DirectiveKind::NewSong);
3602 assert_eq!(DirectiveKind::from_name("Ns"), DirectiveKind::NewSong);
3603 }
3604
3605 #[test]
3606 fn new_song_canonical_name() {
3607 assert_eq!(DirectiveKind::NewSong.canonical_name(), "new_song");
3608 }
3609
3610 #[test]
3611 fn new_song_parsed_as_directive() {
3612 let result = lines("{new_song}");
3613 assert_eq!(result.len(), 1);
3614 if let Line::Directive(ref d) = result[0] {
3615 assert_eq!(d.name, "new_song");
3616 assert_eq!(d.kind, DirectiveKind::NewSong);
3617 assert!(d.value.is_none());
3618 } else {
3619 panic!("expected directive");
3620 }
3621 }
3622
3623 #[test]
3624 fn selector_suffix_on_comment_directive() {
3625 let result = lines("{comment-piano: Play softly}");
3628 if let Line::Directive(ref d) = result[0] {
3629 assert_eq!(d.kind, DirectiveKind::Comment);
3630 assert_eq!(d.value.as_deref(), Some("Play softly"));
3631 assert_eq!(d.selector.as_deref(), Some("piano"));
3632 } else {
3633 panic!(
3634 "expected directive for comment with selector, got {:?}",
3635 result[0]
3636 );
3637 }
3638 }
3639
3640 #[test]
3641 fn ns_alias_parsed_as_directive() {
3642 let result = lines("{ns}");
3643 assert_eq!(result.len(), 1);
3644 if let Line::Directive(ref d) = result[0] {
3645 assert_eq!(d.name, "new_song");
3646 assert_eq!(d.kind, DirectiveKind::NewSong);
3647 } else {
3648 panic!("expected directive");
3649 }
3650 }
3651
3652 #[test]
3655 fn parse_multi_single_song() {
3656 let input = "{title: Only Song}\n[G]Hello";
3657 let songs = parse_multi(input).unwrap();
3658 assert_eq!(songs.len(), 1);
3659 assert_eq!(songs[0].metadata.title.as_deref(), Some("Only Song"));
3660 }
3661
3662 #[test]
3663 fn parse_multi_two_songs() {
3664 let input = "{title: Song One}\nLyrics one\n{new_song}\n{title: Song Two}\nLyrics two";
3665 let songs = parse_multi(input).unwrap();
3666 assert_eq!(songs.len(), 2);
3667 assert_eq!(songs[0].metadata.title.as_deref(), Some("Song One"));
3668 assert_eq!(songs[1].metadata.title.as_deref(), Some("Song Two"));
3669 }
3670
3671 #[test]
3672 fn parse_multi_ns_alias() {
3673 let input = "{title: First}\n{ns}\n{title: Second}";
3674 let songs = parse_multi(input).unwrap();
3675 assert_eq!(songs.len(), 2);
3676 assert_eq!(songs[0].metadata.title.as_deref(), Some("First"));
3677 assert_eq!(songs[1].metadata.title.as_deref(), Some("Second"));
3678 }
3679
3680 #[test]
3681 fn parse_multi_three_songs() {
3682 let input = "{title: A}\n{new_song}\n{title: B}\n{new_song}\n{title: C}";
3683 let songs = parse_multi(input).unwrap();
3684 assert_eq!(songs.len(), 3);
3685 assert_eq!(songs[0].metadata.title.as_deref(), Some("A"));
3686 assert_eq!(songs[1].metadata.title.as_deref(), Some("B"));
3687 assert_eq!(songs[2].metadata.title.as_deref(), Some("C"));
3688 }
3689
3690 #[test]
3691 fn parse_multi_empty_first_song() {
3692 let input = "{new_song}\n{title: Second}";
3694 let songs = parse_multi(input).unwrap();
3695 assert_eq!(songs.len(), 2);
3696 assert!(songs[0].metadata.title.is_none());
3697 assert_eq!(songs[1].metadata.title.as_deref(), Some("Second"));
3698 }
3699
3700 #[test]
3701 fn parse_multi_case_insensitive() {
3702 let input = "{title: A}\n{NEW_SONG}\n{title: B}";
3703 let songs = parse_multi(input).unwrap();
3704 assert_eq!(songs.len(), 2);
3705 }
3706
3707 #[test]
3708 fn parse_multi_with_whitespace() {
3709 let input = "{title: A}\n{ new_song }\n{title: B}";
3710 let songs = parse_multi(input).unwrap();
3711 assert_eq!(songs.len(), 2);
3712 }
3713
3714 #[test]
3715 fn parse_multi_crlf_line_endings() {
3716 let input = "{title: A}\r\n[G]Hello\r\n{new_song}\r\n{title: B}\r\n[Am]World\r\n";
3717 let songs = parse_multi(input).unwrap();
3718 assert_eq!(songs.len(), 2);
3719 assert_eq!(songs[0].metadata.title, Some("A".to_string()));
3720 assert_eq!(songs[1].metadata.title, Some("B".to_string()));
3721 }
3722
3723 #[test]
3724 fn parse_multi_lenient_collects_errors() {
3725 let input = "{title: Good}\n[Am\n{new_song}\n{title: Also Good}\n[G]Hello";
3726 let result = parse_multi_lenient(input);
3727 assert_eq!(result.results.len(), 2);
3728 assert!(result.results[0].has_errors()); assert!(result.results[1].is_ok());
3730 assert_eq!(
3731 result.results[1].song.metadata.title.as_deref(),
3732 Some("Also Good")
3733 );
3734 }
3735
3736 #[test]
3737 fn comment_without_selector_still_becomes_line_comment() {
3738 let result = lines("{comment: Normal comment}");
3739 assert!(
3740 matches!(result[0], Line::Comment(CommentStyle::Normal, _)),
3741 "comment without selector should still be Line::Comment"
3742 );
3743 }
3744
3745 #[test]
3746 fn parse_multi_songs_helper() {
3747 let input = "{title: A}\n{new_song}\n{title: B}";
3748 let result = parse_multi_lenient(input);
3749 let songs = result.songs();
3750 assert_eq!(songs.len(), 2);
3751 assert_eq!(songs[0].metadata.title.as_deref(), Some("A"));
3752 assert_eq!(songs[1].metadata.title.as_deref(), Some("B"));
3753 }
3754
3755 #[test]
3756 fn parse_multi_preserves_song_content() {
3757 let input = "{title: Song One}
3758{artist: Artist One}
3759{start_of_chorus}
3760[G]La la [C]la
3761{end_of_chorus}
3762{new_song}
3763{title: Song Two}
3764{key: Am}
3765[Am]Hello [G]world";
3766 let songs = parse_multi(input).unwrap();
3767 assert_eq!(songs.len(), 2);
3768
3769 assert_eq!(songs[0].metadata.title.as_deref(), Some("Song One"));
3771 assert_eq!(songs[0].metadata.artists, vec!["Artist One".to_string()]);
3772
3773 assert_eq!(songs[1].metadata.title.as_deref(), Some("Song Two"));
3775 assert_eq!(songs[1].metadata.key.as_deref(), Some("Am"));
3776 }
3777
3778 #[test]
3779 fn is_new_song_line_detection() {
3780 assert!(is_new_song_line("{new_song}"));
3781 assert!(is_new_song_line("{ns}"));
3782 assert!(is_new_song_line("{NEW_SONG}"));
3783 assert!(is_new_song_line("{NS}"));
3784 assert!(is_new_song_line("{ new_song }"));
3785 assert!(is_new_song_line("{ ns }"));
3786 assert!(is_new_song_line("{new_song: value}"));
3788 assert!(is_new_song_line("{ns: tag}"));
3789 assert!(is_new_song_line("{ new_song : tag }"));
3790
3791 assert!(!is_new_song_line("{title}"));
3792 assert!(!is_new_song_line("new_song"));
3793 assert!(!is_new_song_line(""));
3794 assert!(!is_new_song_line("{new_songs}"));
3795 }
3796
3797 #[test]
3798 fn split_at_new_song_bare_cr() {
3799 let input = "{title: A}\r{new_song}\r{title: B}";
3801 let segments = split_at_new_song(input);
3802 assert_eq!(segments.len(), 2);
3803 assert!(segments[0].contains("title: A"));
3804 assert!(segments[1].contains("title: B"));
3805 }
3806
3807 #[test]
3808 fn single_parse_ignores_new_song() {
3809 let song = parse("{title: Test}\n{new_song}\n[G]Hello").unwrap();
3812 assert_eq!(song.metadata.title.as_deref(), Some("Test"));
3813 let has_new_song = song
3815 .lines
3816 .iter()
3817 .any(|l| matches!(l, Line::Directive(d) if d.kind == DirectiveKind::NewSong));
3818 assert!(has_new_song);
3819 }
3820
3821 #[test]
3822 fn selector_suffix_on_environment_directive() {
3823 let result = lines("{start_of_chorus-piano}");
3824 if let Line::Directive(ref d) = result[0] {
3825 assert_eq!(d.name, "start_of_chorus");
3826 assert_eq!(d.kind, DirectiveKind::StartOfChorus);
3827 assert_eq!(d.selector.as_deref(), Some("piano"));
3828 } else {
3829 panic!("expected directive");
3830 }
3831 }
3832
3833 #[test]
3834 fn selector_suffix_on_end_environment() {
3835 let result = lines("{end_of_verse-guitar}");
3836 if let Line::Directive(ref d) = result[0] {
3837 assert_eq!(d.name, "end_of_verse");
3838 assert_eq!(d.kind, DirectiveKind::EndOfVerse);
3839 assert_eq!(d.selector.as_deref(), Some("guitar"));
3840 } else {
3841 panic!("expected directive");
3842 }
3843 }
3844
3845 #[test]
3846 fn no_selector_on_plain_directive() {
3847 let result = lines("{title: My Song}");
3848 if let Line::Directive(ref d) = result[0] {
3849 assert_eq!(d.selector, None);
3850 } else {
3851 panic!("expected directive");
3852 }
3853 }
3854
3855 #[test]
3856 fn selector_suffix_case_insensitive() {
3857 let result = lines("{Title-PIANO: My Song}");
3858 if let Line::Directive(ref d) = result[0] {
3859 assert_eq!(d.kind, DirectiveKind::Title);
3860 assert_eq!(d.selector.as_deref(), Some("piano"));
3861 } else {
3862 panic!("expected directive");
3863 }
3864 }
3865
3866 #[test]
3867 fn selector_with_short_alias() {
3868 let result = lines("{t-guitar: My Song}");
3869 if let Line::Directive(ref d) = result[0] {
3870 assert_eq!(d.name, "title");
3871 assert_eq!(d.kind, DirectiveKind::Title);
3872 assert_eq!(d.selector.as_deref(), Some("guitar"));
3873 } else {
3874 panic!("expected directive");
3875 }
3876 }
3877
3878 #[test]
3879 fn unknown_directive_with_hyphen_no_selector() {
3880 let result = lines("{my-custom: value}");
3882 if let Line::Directive(ref d) = result[0] {
3883 assert_eq!(d.kind, DirectiveKind::Unknown("my-custom".to_string()));
3884 assert_eq!(d.selector, None);
3885 } else {
3886 panic!("expected directive");
3887 }
3888 }
3889
3890 #[test]
3891 fn custom_section_with_selector() {
3892 let result = lines("{start_of_intro-piano}");
3893 if let Line::Directive(ref d) = result[0] {
3894 assert_eq!(d.kind, DirectiveKind::StartOfSection("intro".to_string()));
3895 assert_eq!(d.selector.as_deref(), Some("piano"));
3896 } else {
3897 panic!("expected directive");
3898 }
3899 }
3900
3901 #[test]
3902 #[should_panic(expected = "token list must contain at least an Eof token")]
3903 fn parser_new_panics_on_empty_tokens() {
3904 let _parser = Parser::new(Vec::new());
3905 }
3906
3907 #[test]
3910 fn multi_song_oversized_input_rejected() {
3911 let opts = ParseOptions {
3912 max_input_size: 10,
3913 ..Default::default()
3914 };
3915 let input = "{title: A}\n{new_song}\n{title: B}";
3916 let result = parse_multi_with_options(input, &opts);
3917 assert!(result.is_err());
3918 assert!(result.unwrap_err().message.contains("exceeds maximum"));
3919 }
3920
3921 #[test]
3922 fn multi_song_lenient_oversized_input_rejected() {
3923 let opts = ParseOptions {
3924 max_input_size: 10,
3925 ..Default::default()
3926 };
3927 let input = "{title: A}\n{new_song}\n{title: B}";
3928 let result = parse_multi_lenient_with_options(input, &opts);
3929 assert_eq!(result.results.len(), 1);
3930 assert!(
3931 result.results[0].errors[0]
3932 .message
3933 .contains("exceeds maximum")
3934 );
3935 }
3936
3937 #[test]
3938 fn multi_song_within_limit_succeeds() {
3939 let opts = ParseOptions {
3940 max_input_size: 1000,
3941 ..Default::default()
3942 };
3943 let input = "{title: A}\n{new_song}\n{title: B}";
3944 let result = parse_multi_with_options(input, &opts);
3945 assert!(result.is_ok());
3946 assert_eq!(result.unwrap().len(), 2);
3947 }
3948
3949 #[test]
3952 fn config_override_basic() {
3953 let result = lines("{+config.pdf.margins.top: 100}");
3954 if let Line::Directive(ref d) = result[0] {
3955 assert_eq!(
3956 d.kind,
3957 DirectiveKind::ConfigOverride("pdf.margins.top".to_string())
3958 );
3959 assert_eq!(d.value.as_deref(), Some("100"));
3960 assert_eq!(d.name, "+config.pdf.margins.top");
3961 } else {
3962 panic!("expected directive");
3963 }
3964 }
3965
3966 #[test]
3967 fn config_override_string_value() {
3968 let result = lines("{+config.pdf.theme.foreground: blue}");
3969 if let Line::Directive(ref d) = result[0] {
3970 assert_eq!(
3971 d.kind,
3972 DirectiveKind::ConfigOverride("pdf.theme.foreground".to_string())
3973 );
3974 assert_eq!(d.value.as_deref(), Some("blue"));
3975 } else {
3976 panic!("expected directive");
3977 }
3978 }
3979
3980 #[test]
3981 fn config_override_no_value() {
3982 let result = lines("{+config.settings.lyrics_only}");
3983 if let Line::Directive(ref d) = result[0] {
3984 assert_eq!(
3985 d.kind,
3986 DirectiveKind::ConfigOverride("settings.lyrics_only".to_string())
3987 );
3988 assert_eq!(d.value, None);
3989 } else {
3990 panic!("expected directive");
3991 }
3992 }
3993
3994 #[test]
3995 fn config_override_case_insensitive() {
3996 let result = lines("{+Config.PDF.Margins.Top: 50}");
3997 if let Line::Directive(ref d) = result[0] {
3998 assert_eq!(
3999 d.kind,
4000 DirectiveKind::ConfigOverride("pdf.margins.top".to_string())
4001 );
4002 } else {
4003 panic!("expected directive");
4004 }
4005 }
4006
4007 #[test]
4008 fn bare_plus_config_is_unknown() {
4009 let result = lines("{+config: something}");
4011 if let Line::Directive(ref d) = result[0] {
4012 assert!(matches!(d.kind, DirectiveKind::Unknown(_)));
4013 } else {
4014 panic!("expected directive");
4015 }
4016 }
4017
4018 #[test]
4019 fn config_overrides_extracted_from_song() {
4020 let song = crate::parse(
4021 "{title: Test}\n{+config.pdf.margins.top: 100}\n{+config.settings.transpose: 2}\n",
4022 )
4023 .unwrap();
4024 let overrides = song.config_overrides();
4025 assert_eq!(overrides.len(), 2);
4026 assert_eq!(overrides[0], ("pdf.margins.top", "100"));
4027 assert_eq!(overrides[1], ("settings.transpose", "2"));
4028 }
4029
4030 #[test]
4031 fn config_overrides_empty_when_none() {
4032 let song = crate::parse("{title: Test}\n[G]Hello\n").unwrap();
4033 assert!(song.config_overrides().is_empty());
4034 }
4035
4036 #[test]
4037 fn config_overrides_not_in_multi_song_leak() {
4038 let songs = crate::parse_multi(
4039 "{title: A}\n{+config.settings.transpose: 5}\n{new_song}\n{title: B}\n",
4040 )
4041 .unwrap();
4042 assert_eq!(songs.len(), 2);
4043 assert_eq!(songs[0].config_overrides().len(), 1);
4044 assert!(songs[1].config_overrides().is_empty());
4045 }
4046}
4047
4048#[cfg(test)]
4049mod metadata_cap_tests {
4050 use super::*;
4051
4052 #[test]
4053 fn test_metadata_entries_capped_at_limit() {
4054 let count = Parser::MAX_METADATA_ENTRIES + 100;
4056 let mut input = String::new();
4057 for i in 0..count {
4058 input.push_str(&format!("{{subtitle: sub{i}}}\n"));
4059 }
4060 let song = parse(&input).unwrap();
4061 assert_eq!(
4062 song.metadata.subtitles.len(),
4063 Parser::MAX_METADATA_ENTRIES,
4064 "subtitles should be capped at MAX_METADATA_ENTRIES"
4065 );
4066 }
4067
4068 #[test]
4069 fn test_metadata_cap_applies_per_field() {
4070 let mut input = String::new();
4072 for i in 0..Parser::MAX_METADATA_ENTRIES {
4073 input.push_str(&format!("{{subtitle: s{i}}}\n"));
4074 }
4075 input.push_str("{artist: Alice}\n");
4076 let song = parse(&input).unwrap();
4077 assert_eq!(song.metadata.subtitles.len(), Parser::MAX_METADATA_ENTRIES);
4078 assert_eq!(song.metadata.artists.len(), 1);
4079 }
4080
4081 #[test]
4082 fn test_metadata_cap_via_meta_directive() {
4083 let count = Parser::MAX_METADATA_ENTRIES + 50;
4085 let mut input = String::new();
4086 for i in 0..count {
4087 input.push_str(&format!("{{meta: subtitle s{i}}}\n"));
4088 }
4089 let song = parse(&input).unwrap();
4090 assert_eq!(
4091 song.metadata.subtitles.len(),
4092 Parser::MAX_METADATA_ENTRIES,
4093 "meta directive path should also cap subtitles"
4094 );
4095 }
4096}