1use crate::bar::BarlineType;
23use crate::chord::Chord;
24use crate::clef::Clef;
25use crate::defaults::FloatType;
26use crate::duration::Duration;
27use crate::error::{Error, Result};
28use crate::expressions::Expression;
29use crate::key::Key;
30use crate::metadata::Metadata;
31use crate::meter::TimeSignature;
32use crate::notation::{Beams, StemDirection, Syllabic, TieType};
33use crate::note::Note;
34use crate::pitch::Pitch;
35use crate::rest::Rest;
36use crate::stream::{Stream, StreamElement, StreamKind};
37use crate::tempo::MetronomeMark;
38
39#[derive(Clone, Debug, PartialEq, Eq)]
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
42#[non_exhaustive]
43pub struct LilyOptions {
44 pub version: (u32, u32),
47}
48
49impl Default for LilyOptions {
50 fn default() -> Self {
51 Self { version: (2, 24) }
52 }
53}
54
55fn lily_error(message: impl Into<String>) -> Error {
56 Error::Notation(message.into())
57}
58
59#[derive(Clone, Debug)]
63enum Item {
64 Node(usize),
65 Text(String),
66}
67
68#[derive(Clone, Debug)]
70enum Kind {
71 Top {
72 contents: Vec<Item>,
73 },
74 Header {
75 body: Option<usize>,
76 },
77 HeaderBody {
78 assignments: Vec<Item>,
79 },
80 Assignment {
81 id: String,
82 value: String,
83 },
84 Embedded(String),
85 ScoreBlock {
86 body: usize,
87 },
88 ScoreBody {
89 music: usize,
90 },
91 OutputDef,
92 Layout,
93 MusicList {
94 contents: Vec<Item>,
95 },
96 Music {
97 composite: usize,
98 },
99 Sequential {
100 list: usize,
101 start_staff: bool,
102 },
103 Simultaneous {
104 list: usize,
105 },
106 SimpleMusic {
107 chord: usize,
108 },
109 ContextModification {
110 list: Vec<String>,
111 },
112 Composite {
113 prefix: Option<usize>,
114 grouped: Option<usize>,
115 lyrics: Option<usize>,
116 },
117 Grouped {
118 simultaneous: Option<usize>,
119 sequential: Option<usize>,
120 },
121 OptionalId(String),
122 Prefix {
123 kind: &'static str,
124 context: String,
125 id: Option<usize>,
126 modification: Option<usize>,
127 music: usize,
128 fraction: String,
129 },
130 NewLyrics {
131 lists: Vec<usize>,
132 },
133 PropertySet {
134 property: String,
135 value: String,
136 },
137 EventChord {
138 simple: Option<usize>,
139 post: Vec<String>,
140 note_chord: Option<usize>,
141 },
142 NoteChordElement {
143 body: usize,
144 duration: usize,
145 post: Vec<String>,
146 },
147 ChordBody {
148 elements: Vec<usize>,
149 },
150 ChordBodyElement {
151 parts: Vec<Item>,
152 },
153 Pitch {
154 name: String,
155 quotes: String,
156 },
157 Duration {
158 number: String,
159 dots: u32,
160 },
161 SimpleElement {
162 parts: Vec<Item>,
163 },
164 LyricElement(String),
165}
166
167#[derive(Clone, Debug)]
168struct Node {
169 parent: Option<usize>,
170 kind: Kind,
171}
172
173#[derive(Default)]
174struct Tree {
175 nodes: Vec<Node>,
176}
177
178impl Tree {
179 fn add(&mut self, kind: Kind) -> usize {
183 let index = self.nodes.len();
184 let (attributes, listed) = children(&kind);
185 self.nodes.push(Node { parent: None, kind });
186 for child in attributes {
187 self.nodes[child].parent = Some(index);
188 }
189 for child in listed {
190 self.adopt(index, child);
191 }
192 index
193 }
194
195 fn set_parent(&mut self, child: usize, parent: usize) {
197 self.nodes[child].parent = Some(parent);
198 }
199
200 fn adopt(&mut self, parent: usize, child: usize) {
203 if self.nodes[child].parent.is_none() {
204 self.nodes[child].parent = Some(parent);
205 }
206 }
207
208 fn contents_mut(&mut self, node: usize) -> Result<&mut Vec<Item>> {
209 match &mut self.nodes[node].kind {
210 Kind::MusicList { contents } | Kind::Top { contents } => Ok(contents),
211 _ => Err(lily_error(
212 "Cannot get a currentMusicList from contextObject",
213 )),
214 }
215 }
216
217 fn newline_indent(&self, node: usize) -> String {
219 let mut depth = 0;
220 let mut current = self.nodes[node].parent;
221 while let Some(parent) = current {
222 depth += 1;
223 current = self.nodes[parent].parent;
224 }
225 format!("\n{}", " ".repeat(depth))
226 }
227
228 fn str_of(&self, node: usize) -> String {
230 self.string_output(node).replace("\n\n", "\n")
231 }
232
233 fn item(&self, item: &Item) -> String {
234 match item {
235 Item::Node(node) => self.str_of(*node),
236 Item::Text(text) => text.clone(),
237 }
238 }
239
240 fn newline_separated(&self, node: usize, items: &[Item]) -> String {
242 let indent = self.newline_indent(node);
243 items.iter().map(|item| self.item(item) + &indent).collect()
244 }
245
246 fn enclose(&self, node: usize, inner: Option<String>) -> String {
248 let Some(inner) = inner else {
249 return " { } ".to_string();
250 };
251 let indent = self.newline_indent(node);
252 format!(" {{ {indent}{inner}{indent} }} {indent}")
253 }
254
255 fn string_output(&self, node: usize) -> String {
256 match &self.nodes[node].kind {
257 Kind::Top { contents } => self.newline_separated(node, contents),
258 Kind::Header { body } => {
259 format!(
260 "\\header{}",
261 self.enclose(node, body.map(|body| self.str_of(body)))
262 )
263 }
264 Kind::HeaderBody { assignments } => self.newline_separated(node, assignments),
265 Kind::Assignment { id, value } => [id.as_str(), "=", "e(value), " "].join(" "),
266 Kind::Embedded(content) => content.clone(),
267 Kind::ScoreBlock { body } => {
268 format!("\\score {}", self.enclose(node, Some(self.str_of(*body))))
269 }
270 Kind::ScoreBody { music } | Kind::Music { composite: music } => {
271 self.string_output(*music)
272 }
273 Kind::OutputDef => "\\paper { }".to_string(),
274 Kind::Layout => {
275 let lines = [
276 "\\layout {",
277 " \\context {",
278 " \\RemoveEmptyStaves",
279 " \\override VerticalAxisGroup.remove-first = ##t",
280 " }",
281 "}",
282 ];
283 let indent = self.newline_indent(node);
284 lines.iter().map(|line| format!("{line}{indent}")).collect()
285 }
286 Kind::MusicList { contents } => self.newline_separated(node, contents),
287 Kind::Sequential { list, start_staff } => format!(
288 "{{ {}{} }} {}",
289 if *start_staff { "\\startStaff " } else { "" },
290 self.string_output(*list),
291 self.newline_indent(node)
292 ),
293 Kind::Simultaneous { list } => {
294 let indent = self.newline_indent(node);
295 format!("{indent}<< {} >>{indent}", self.string_output(*list))
296 }
297 Kind::SimpleMusic { chord } => self.string_output(*chord),
298 Kind::ContextModification { list } => {
299 let indent = self.newline_indent(node);
300 format!(
301 "\\with {{ {indent}{}{indent} }} {indent}",
302 list.join(&indent)
303 )
304 }
305 Kind::Composite {
306 prefix,
307 grouped,
308 lyrics,
309 } => {
310 let lyrics = lyrics.map(|lyrics| self.str_of(lyrics)).unwrap_or_default();
311 let music = prefix.or(*grouped).expect("composite music holds music");
312 format!("{}\n{lyrics}", self.str_of(music))
313 }
314 Kind::Grouped {
315 simultaneous,
316 sequential,
317 } => self.str_of(
318 simultaneous
319 .or(*sequential)
320 .expect("grouped music holds music"),
321 ),
322 Kind::OptionalId(content) => format!(" = {content}"),
323 Kind::Prefix {
324 kind,
325 context,
326 id,
327 modification,
328 music,
329 fraction,
330 } => {
331 if *kind == "tuplet" {
332 return format!("\\tuplet {fraction} {} ", self.str_of(*music));
333 }
334 let mut out = format!("\\{kind} {context} ");
335 if let Some(id) = id {
336 out += &(self.str_of(*id) + " ");
337 }
338 if let Some(modification) = modification {
339 out += &(self.str_of(*modification) + " ");
340 }
341 out + &self.str_of(*music) + " "
342 }
343 Kind::NewLyrics { lists } => lists
344 .iter()
345 .map(|list| format!("\\addlyrics {}", self.string_output(*list)))
346 .collect(),
347 Kind::PropertySet { property, value } => format!("\\set {property} = {value} "),
348 Kind::EventChord {
349 simple,
350 post,
351 note_chord,
352 } => {
353 if let Some(note_chord) = note_chord {
354 return self.str_of(*note_chord) + " ";
355 }
356 let mut out = simple.map(|simple| self.str_of(simple)).unwrap_or_default();
357 for event in post {
358 out += &format!(" {event}");
359 }
360 out + " "
361 }
362 Kind::NoteChordElement {
363 body,
364 duration,
365 post,
366 } => {
367 let mut out = self.str_of(*body);
368 out += &(self.str_of(*duration) + " ");
369 for event in post {
370 out += &format!("{event} ");
371 }
372 out
373 }
374 Kind::ChordBody { elements } => {
375 let inner: Vec<String> = elements
376 .iter()
377 .map(|element| self.str_of(*element))
378 .collect();
379 ["<", &inner.join(" "), "> "].join(" ")
380 }
381 Kind::ChordBodyElement { parts } => parts
382 .iter()
383 .map(|part| self.item(part))
384 .collect::<Vec<_>>()
385 .join(" "),
386 Kind::Pitch { name, quotes } => format!("{name}{quotes} "),
387 Kind::Duration { number, dots } => {
388 format!("{number}{} ", ".".repeat(*dots as usize))
389 }
390 Kind::SimpleElement { parts } => parts.iter().map(|part| self.item(part)).collect(),
391 Kind::LyricElement(text) => format!("{text} "),
392 }
393 }
394}
395
396fn children(kind: &Kind) -> (Vec<usize>, Vec<usize>) {
398 let nodes = |items: &[Item]| -> Vec<usize> {
399 items
400 .iter()
401 .filter_map(|item| match item {
402 Item::Node(node) => Some(*node),
403 Item::Text(_) => None,
404 })
405 .collect()
406 };
407 match kind {
408 Kind::Top { contents } | Kind::MusicList { contents } => (Vec::new(), nodes(contents)),
409 Kind::HeaderBody { assignments } => (Vec::new(), nodes(assignments)),
410 Kind::Header { body } => (body.iter().copied().collect(), Vec::new()),
411 Kind::ScoreBlock { body } => (vec![*body], Vec::new()),
412 Kind::ScoreBody { music } | Kind::Music { composite: music } => (vec![*music], Vec::new()),
413 Kind::Sequential { list, .. } | Kind::Simultaneous { list } => (vec![*list], Vec::new()),
414 Kind::SimpleMusic { chord } => (vec![*chord], Vec::new()),
415 Kind::Composite {
416 prefix,
417 grouped,
418 lyrics,
419 } => (
420 [*prefix, *grouped, *lyrics].into_iter().flatten().collect(),
421 Vec::new(),
422 ),
423 Kind::Grouped {
424 simultaneous,
425 sequential,
426 } => (
427 [*simultaneous, *sequential].into_iter().flatten().collect(),
428 Vec::new(),
429 ),
430 Kind::Prefix {
431 id,
432 modification,
433 music,
434 ..
435 } => (
436 [*id, *modification, Some(*music)]
437 .into_iter()
438 .flatten()
439 .collect(),
440 Vec::new(),
441 ),
442 Kind::NewLyrics { lists } => (Vec::new(), lists.clone()),
443 Kind::EventChord {
444 simple, note_chord, ..
445 } => (
446 [*simple, *note_chord].into_iter().flatten().collect(),
447 Vec::new(),
448 ),
449 Kind::NoteChordElement { body, duration, .. } => (vec![*body, *duration], Vec::new()),
450 Kind::ChordBody { elements } => (Vec::new(), elements.clone()),
451 Kind::ChordBodyElement { parts } | Kind::SimpleElement { parts } => {
452 (Vec::new(), nodes(parts))
453 }
454 _ => (Vec::new(), Vec::new()),
455 }
456}
457
458fn quote(text: &str) -> String {
460 format!("{} ", string_literal(text))
461}
462
463fn string_literal(text: &str) -> String {
466 format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
467}
468
469fn letters_only(text: &str) -> String {
471 text.chars()
472 .map(|c| {
473 if c.is_alphabetic() {
474 c
475 } else {
476 char::from_u32(c as u32 % 26 + 97).unwrap_or('a')
477 }
478 })
479 .collect()
480}
481
482const COLOR_DEFINITION: &str = "color = #(define-music-function (color) (string?) #{\n \\once \\override NoteHead.color = #(x11-color color)\n \\once \\override Stem.color = #(x11-color color)\n \\once \\override Rest.color = #(x11-color color)\n \\once \\override Beam.color = #(x11-color color)\n #})\n ";
483const BOOK_HEADER: &str = "\\include \"lilypond-book-preamble.ly\"\n ";
484
485struct Converter<'a> {
489 tree: Tree,
490 top: usize,
491 context: usize,
492 stored: Vec<usize>,
493 current_measure: Option<&'a Stream>,
494 in_word: bool,
495 meter: Option<TimeSignature>,
498 unnamed: usize,
501}
502
503fn stream_id(stream: &Stream, unnamed: &mut usize) -> String {
505 match stream.id() {
506 Some(id) => id.to_string(),
507 None => {
508 *unnamed += 1;
509 format!("{}", 1_000_000 + *unnamed)
510 }
511 }
512}
513
514fn is_part(stream: &Stream) -> bool {
515 matches!(stream.kind(), StreamKind::Part | StreamKind::PartStaff)
516}
517
518impl<'a> Converter<'a> {
519 fn new() -> Self {
520 let mut tree = Tree::default();
521 let top = tree.add(Kind::Top {
522 contents: Vec::new(),
523 });
524 Self {
525 tree,
526 top,
527 context: top,
528 stored: Vec::new(),
529 current_measure: None,
530 in_word: false,
531 meter: None,
532 unnamed: 0,
533 }
534 }
535
536 fn new_context(&mut self, context: usize) {
537 self.stored.push(self.context);
538 self.context = context;
539 }
540
541 fn restore_context(&mut self) {
542 self.context = self.stored.pop().unwrap_or(self.top);
543 }
544
545 fn append_to_context(&mut self, node: usize) -> Result<()> {
548 let context = self.context;
549 self.tree.contents_mut(context)?.push(Item::Node(node));
550 self.tree.set_parent(node, context);
551 Ok(())
552 }
553
554 fn embedded(&mut self, content: impl Into<String>) -> usize {
555 self.tree.add(Kind::Embedded(content.into()))
556 }
557
558 fn load_score(&mut self, score: &'a Stream, options: &LilyOptions) -> Result<()> {
560 let version = self.embedded(format!(
561 "\\version {}",
562 quote(&format!("{}.{}", options.version.0, options.version.1))
563 ));
564 let book = self.embedded(BOOK_HEADER);
565 let colors = self.embedded(COLOR_DEFINITION);
566 let header = self.tree.add(Kind::Header { body: None });
567 let score_block = self.score_block(score)?;
568 let output = self.tree.add(Kind::OutputDef);
569 let layout = self.tree.add(Kind::Layout);
570 let contents: Vec<Item> = [version, book, colors, header, score_block, output, layout]
571 .into_iter()
572 .map(Item::Node)
573 .collect();
574 if let Some(metadata) = score.metadata() {
575 self.set_header(metadata, header);
576 }
577 let context = self.context;
580 if matches!(self.tree.nodes[context].kind, Kind::Composite { .. }) {
581 return Err(lily_error(
582 "a tuplet opened and never closed, or closed and never opened, leaves nowhere to write the score",
583 ));
584 }
585 for item in &contents {
586 if let Item::Node(node) = item {
587 self.tree.adopt(context, *node);
588 }
589 }
590 *self.tree.contents_mut(context)? = contents;
591 Ok(())
592 }
593
594 fn set_header(&mut self, metadata: &Metadata, header: usize) {
596 let body = self.tree.add(Kind::HeaderBody {
597 assignments: Vec::new(),
598 });
599 if let Kind::Header { body: held } = &mut self.tree.nodes[header].kind {
600 *held = Some(body);
601 }
602 self.tree.set_parent(body, header);
603 let best = ["title", "popularTitle", "alternativeTitle", "movementName"]
604 .iter()
605 .find_map(|name| {
606 metadata
607 .string_value(name)
608 .filter(|title| !title.is_empty())
609 });
610 for (id, value) in [
611 ("title", best),
612 (
613 "subtitle",
614 metadata
615 .string_value("alternativeTitle")
616 .filter(|title| !title.is_empty()),
617 ),
618 ] {
619 let Some(value) = value else {
620 continue;
621 };
622 let assignment = self.tree.add(Kind::Assignment {
623 id: id.to_string(),
624 value,
625 });
626 if let Kind::HeaderBody { assignments } = &mut self.tree.nodes[body].kind {
627 assignments.push(Item::Node(assignment));
628 }
629 self.tree.set_parent(assignment, body);
630 }
631 }
632
633 fn score_block(&mut self, score: &'a Stream) -> Result<usize> {
635 let composite = self.tree.add(Kind::Composite {
636 prefix: None,
637 grouped: None,
638 lyrics: None,
639 });
640 self.new_context(composite);
641 let parts: Vec<&'a Stream> = score
642 .events()
643 .iter()
644 .filter_map(|event| match event.element() {
645 StreamElement::Stream(inner) if is_part(inner) => Some(&**inner),
646 _ => None,
647 })
648 .collect();
649 if parts.is_empty() {
650 let prefix = self.prefix_from_stream(score, None, None, false)?;
651 if let Kind::Composite { prefix: held, .. } = &mut self.tree.nodes[composite].kind {
652 *held = Some(prefix);
653 }
654 self.tree.set_parent(prefix, composite);
655 } else {
656 let grouped = self.grouped_from_parts(&parts)?;
657 if let Kind::Composite { grouped: held, .. } = &mut self.tree.nodes[composite].kind {
658 *held = Some(grouped);
659 }
660 self.tree.set_parent(grouped, composite);
661 }
662 let music = self.tree.add(Kind::Music { composite });
663 let body = self.tree.add(Kind::ScoreBody { music });
664 let block = self.tree.add(Kind::ScoreBlock { body });
665 self.restore_context();
666 Ok(block)
667 }
668
669 fn grouped_from_parts(&mut self, parts: &[&'a Stream]) -> Result<usize> {
671 let grouped = self.tree.add(Kind::Grouped {
672 simultaneous: None,
673 sequential: None,
674 });
675 let list = self.tree.add(Kind::MusicList {
676 contents: Vec::new(),
677 });
678 let simultaneous = self.tree.add(Kind::Simultaneous { list });
679 if let Kind::Grouped {
680 simultaneous: held, ..
681 } = &mut self.tree.nodes[grouped].kind
682 {
683 *held = Some(simultaneous);
684 }
685 self.tree.set_parent(simultaneous, grouped);
686 self.new_context(list);
687 let mut composites = Vec::new();
688 for part in parts {
689 composites.push(self.prefix_from_stream(part, None, None, false)?);
690 }
691 self.restore_context();
692 for composite in &composites {
693 self.tree.adopt(list, *composite);
694 }
695 if let Kind::MusicList { contents } = &mut self.tree.nodes[list].kind {
696 *contents = composites.into_iter().map(Item::Node).collect();
697 }
698 Ok(grouped)
699 }
700
701 fn prefix_from_stream(
703 &mut self,
704 stream: &'a Stream,
705 context_type: Option<&str>,
706 kind: Option<&'static str>,
707 start_staff: bool,
708 ) -> Result<usize> {
709 let id_text = stream_id(stream, &mut self.unnamed);
710 let (context, id) = match context_type {
711 Some(context) => (
712 context.to_string(),
713 Some(self.tree.add(Kind::OptionalId(letters_only(&id_text)))),
714 ),
715 None if is_part(stream) => (
716 "Staff".to_string(),
717 Some(self.tree.add(Kind::OptionalId(letters_only(&id_text)))),
718 ),
719 None => ("Voice".to_string(), None),
720 };
721 let mut modifications = Vec::new();
722 if crate::makenotation::beams_made(stream) {
723 modifications.push("\\autoBeamOff ".to_string());
724 }
725 let lines = staff_lines(stream);
726 if lines != 5 {
727 modifications.push(format!("\\override StaffSymbol.line-count = #{lines}"));
728 }
729 let lyrics = self.new_lyrics(stream, &letters_only(&id_text));
730 let sequential = self.sequential_from_stream(stream, start_staff)?;
731 let grouped = self.tree.add(Kind::Grouped {
732 simultaneous: None,
733 sequential: Some(sequential),
734 });
735 let composite = self.tree.add(Kind::Composite {
736 prefix: None,
737 grouped: Some(grouped),
738 lyrics: Some(lyrics),
739 });
740 let music = self.tree.add(Kind::Music { composite });
741 let modification = (!modifications.is_empty()).then(|| {
742 self.tree.add(Kind::ContextModification {
743 list: modifications,
744 })
745 });
746 Ok(self.tree.add(Kind::Prefix {
747 kind: kind.unwrap_or("new"),
748 context,
749 id,
750 modification,
751 music,
752 fraction: String::new(),
753 }))
754 }
755
756 fn new_lyrics(&mut self, stream: &Stream, id: &str) -> usize {
758 let aligned = format!("#{}", quote(id));
759 let mut lists = Vec::new();
760 for (_, lyrics) in stream_lyrics(stream) {
761 let mut items = vec![Item::Node(self.tree.add(Kind::PropertySet {
762 property: "alignBelowContext".to_string(),
763 value: aligned.clone(),
764 }))];
765 self.in_word = false;
766 for lyric in lyrics {
767 let text = self.lyric_text(lyric);
768 items.push(Item::Node(self.tree.add(Kind::LyricElement(text))));
769 }
770 self.in_word = false;
771 let list = self.tree.add(Kind::MusicList { contents: items });
772 let sequential = self.tree.add(Kind::Sequential {
773 list,
774 start_staff: false,
775 });
776 lists.push(self.tree.add(Kind::Grouped {
777 simultaneous: None,
778 sequential: Some(sequential),
779 }));
780 }
781 self.tree.add(Kind::NewLyrics { lists })
782 }
783
784 fn lyric_text(&mut self, lyric: Option<&crate::notation::Lyric>) -> String {
786 let mut in_word = self.in_word;
787 let text = match lyric {
788 None => " _ ".to_string(),
789 Some(lyric) if lyric.text().is_empty() => " _ ".to_string(),
790 Some(lyric) => {
791 let mut text = string_literal(&lyric.text());
792 match lyric.syllabic() {
793 Syllabic::End => {
794 text += "__";
795 in_word = false;
796 }
797 Syllabic::Begin | Syllabic::Middle => {
798 text += " --";
799 in_word = true;
800 }
801 _ => {}
802 }
803 text
804 }
805 };
806 self.in_word = in_word;
807 text
808 }
809
810 fn sequential_from_stream(&mut self, stream: &'a Stream, start_staff: bool) -> Result<usize> {
812 let list = self.tree.add(Kind::MusicList {
813 contents: Vec::new(),
814 });
815 let sequential = self.tree.add(Kind::Sequential { list, start_staff });
816 self.new_context(list);
817 self.append_stream(stream)?;
818 if let Some(bar) = self.close_measure() {
819 self.tree.contents_mut(list)?.push(Item::Node(bar));
821 }
822 self.restore_context();
823 Ok(sequential)
824 }
825
826 fn append_stream(&mut self, stream: &'a Stream) -> Result<()> {
829 let events = stream.events();
830 let mut start = 0;
831 while start < events.len() {
832 let offset = events[start].offset();
833 let mut end = start + 1;
834 while end < events.len() && events[end].offset() == offset {
835 end += 1;
836 }
837 let group = &events[start..end];
838 if group.len() == 1 {
839 self.append_element(group[0].element())?;
840 } else {
841 let voices: Vec<&'a Stream> = group
842 .iter()
843 .filter_map(|event| match event.element() {
844 StreamElement::Stream(inner) if inner.kind() == StreamKind::Voice => {
845 Some(&**inner)
846 }
847 _ => None,
848 })
849 .collect();
850 if !voices.is_empty() {
851 let grouped = self.tree.add(Kind::Grouped {
852 simultaneous: None,
853 sequential: None,
854 });
855 let list = self.tree.add(Kind::MusicList {
856 contents: Vec::new(),
857 });
858 let simultaneous = self.tree.add(Kind::Simultaneous { list });
859 if let Kind::Grouped {
860 simultaneous: held, ..
861 } = &mut self.tree.nodes[grouped].kind
862 {
863 *held = Some(simultaneous);
864 }
865 self.tree.set_parent(simultaneous, grouped);
866 let mut composites = Vec::new();
867 for voice in voices {
868 composites.push(self.prefix_from_stream(voice, None, None, false)?);
869 }
870 for composite in &composites {
871 self.tree.adopt(list, *composite);
872 }
873 if let Kind::MusicList { contents } = &mut self.tree.nodes[list].kind {
874 *contents = composites.into_iter().map(Item::Node).collect();
875 }
876 self.append_to_context(grouped)?;
877 }
878 for event in group {
879 if !matches!(event.element(), StreamElement::Stream(inner) if inner.kind() == StreamKind::Voice)
880 {
881 self.append_element(event.element())?;
882 }
883 }
884 }
885 start = end;
886 }
887 Ok(())
888 }
889
890 fn append_element(&mut self, element: &'a StreamElement) -> Result<()> {
892 if !matches!(element, StreamElement::Stream(_))
893 && let Some(duration) = element.duration()
894 && duration.is_complex()
895 {
896 for (_, piece) in crate::makenotation::element_at_durations(element, duration)? {
897 self.append_owned(piece)?;
898 }
899 return Ok(());
900 }
901 match element {
902 StreamElement::Stream(measure) if measure.kind() == StreamKind::Measure => {
903 if let Some(bar) = self.close_measure() {
904 self.append_to_context(bar)?;
905 }
906 if let Some(pad) = self.padding(measure)? {
907 self.append_to_context(pad)?;
908 }
909 self.append_stream(measure)?;
910 self.current_measure = Some(measure);
911 }
912 StreamElement::Stream(inner) => {
913 let prefix = self.prefix_from_stream(inner, None, None, false)?;
914 self.append_to_context(prefix)?;
915 }
916 other => self.append_owned(other.clone())?,
917 }
918 Ok(())
919 }
920
921 fn append_owned(&mut self, element: StreamElement) -> Result<()> {
923 match &element {
924 StreamElement::Note(note) => self.append_note(note)?,
925 StreamElement::Rest(rest) => self.append_rest(rest)?,
926 StreamElement::Chord(chord) => self.append_chord(chord, chord.duration())?,
927 StreamElement::ChordSymbol(symbol) => {
929 let chord = Chord::new(symbol.pitches()?)?;
930 self.append_chord(&chord, Some(symbol.duration()))?;
931 }
932 StreamElement::Clef(clef) => {
933 let scheme = self.embedded(clef_scheme(clef));
934 self.append_to_context(scheme)?;
935 }
936 StreamElement::KeySignature(signature) => {
937 let scheme = self.embedded(key_scheme(&signature.as_key("major")));
938 self.append_to_context(scheme)?;
939 }
940 StreamElement::Key(key) => {
941 let scheme = self.embedded(key_scheme(key));
942 self.append_to_context(scheme)?;
943 }
944 StreamElement::TimeSignature(meter) => {
945 self.meter = Some(meter.clone());
946 let scheme = self.embedded(time_scheme(meter));
947 self.append_to_context(scheme)?;
948 }
949 StreamElement::MetronomeMark(mark) => {
950 if let Some(scheme) = tempo_scheme(mark)? {
951 let scheme = self.embedded(scheme);
952 self.append_to_context(scheme)?;
953 }
954 }
955 StreamElement::Layout(crate::layout::Layout::System(_)) => {
956 let scheme = self.embedded("\\break");
957 self.append_to_context(scheme)?;
958 }
959 StreamElement::Layout(crate::layout::Layout::Page(_)) => {
960 let scheme = self.embedded("\\pageBreak");
961 self.append_to_context(scheme)?;
962 }
963 _ => {}
964 }
965 Ok(())
966 }
967
968 fn tuplet_start(&mut self, duration: Option<&Duration>) -> Result<()> {
971 let Some(first) = duration.and_then(|duration| duration.tuplets().into_iter().next())
972 else {
973 return Ok(());
974 };
975 if first.tuplet_type() != Some(crate::duration::TupletType::Start) {
976 return Ok(());
977 }
978 let fraction = format!("{}/{}", first.tuplet_actual().0, first.tuplet_normal().0);
979 let list = self.tree.add(Kind::MusicList {
980 contents: Vec::new(),
981 });
982 let sequential = self.tree.add(Kind::Sequential {
983 list,
984 start_staff: false,
985 });
986 let prefix = self.tree.add(Kind::Prefix {
987 kind: "tuplet",
988 context: String::new(),
989 id: None,
990 modification: None,
991 music: sequential,
992 fraction,
993 });
994 self.append_to_context(prefix)?;
995 self.new_context(list);
996 Ok(())
997 }
998
999 fn tuplet_stop(&mut self, duration: Option<&Duration>) {
1001 if duration
1002 .and_then(|duration| duration.tuplets().into_iter().next())
1003 .is_some_and(|first| first.tuplet_type() == Some(crate::duration::TupletType::Stop))
1004 {
1005 self.restore_context();
1006 }
1007 }
1008
1009 fn beam_code(&mut self, beams: &Beams) -> Result<()> {
1011 let (mut left, mut right) = (0, 0);
1012 for beam in beams.beams() {
1013 match beam.beam_type().map_or("", |kind| kind.as_str()) {
1014 "start" => right += 1,
1015 "continue" => {
1016 right += 1;
1017 left += 1;
1018 }
1019 "stop" => left += 1,
1020 "partial" => {
1021 if beam
1022 .direction()
1023 .is_some_and(|direction| direction.as_str() == "left")
1024 {
1025 left += 1;
1026 } else {
1027 right += 1;
1028 }
1029 }
1030 _ => {}
1031 }
1032 }
1033 if left > 0 {
1034 let scheme = self.embedded(format!("\\set stemLeftBeamCount = #{left}"));
1035 self.append_to_context(scheme)?;
1036 }
1037 if right > 0 {
1038 let scheme = self.embedded(format!("\\set stemRightBeamCount = #{right}"));
1039 self.append_to_context(scheme)?;
1040 }
1041 Ok(())
1042 }
1043
1044 fn stem_code(&mut self, direction: StemDirection) -> Result<()> {
1046 let said = match direction {
1047 StemDirection::Up => "UP",
1048 StemDirection::Down => "DOWN",
1049 _ => return Ok(()),
1050 };
1051 let scheme = self.embedded(format!("\\once \\override Stem.direction = #{said} "));
1052 self.append_to_context(scheme)
1053 }
1054
1055 fn append_note(&mut self, note: &Note) -> Result<()> {
1057 let duration = note.duration();
1058 if duration.is_some_and(Duration::is_grace) {
1059 return Ok(());
1060 }
1061 self.tuplet_start(duration)?;
1062 self.beam_code(note.beams())?;
1063 self.stem_code(note.stem_direction())?;
1064 let mut parts = Vec::new();
1065 if let Some(color) = note.color().filter(|color| !color.is_empty())
1066 && !note.hidden()
1067 {
1068 let color = string_literal(color);
1069 parts.push(Item::Text(format!("\\override NoteHead.color = {color}\n")));
1070 parts.push(Item::Text(format!("\\override Stem.color = {color}\n")));
1071 }
1072 if note.hidden() {
1073 parts.push(Item::Text("s ".to_string()));
1074 } else {
1075 parts.push(Item::Node(self.pitch_node(note.pitch())?));
1076 parts.extend(accidental_marks(note.pitch()));
1077 }
1078 let length = self.duration_node(&element_duration(duration))?;
1079 parts.push(Item::Node(length));
1080 if let Some(first) = note.beams().beams().first() {
1081 match first.beam_type().map_or("", |kind| kind.as_str()) {
1082 "start" => parts.push(Item::Text("[ ".to_string())),
1083 "stop" => parts.push(Item::Text("] ".to_string())),
1084 _ => {}
1085 }
1086 }
1087 let post = post_events(note.tie().map(|tie| tie.tie_type()), note.expressions());
1088 self.push_simple(parts, post)?;
1089 self.tuplet_stop(duration);
1090 Ok(())
1091 }
1092
1093 fn append_rest(&mut self, rest: &Rest) -> Result<()> {
1095 let duration = Some(rest.duration());
1096 if rest.duration().is_grace() {
1097 return Ok(());
1098 }
1099 self.tuplet_start(duration)?;
1100 let mut parts = vec![Item::Text(
1101 if rest.hidden() { "s " } else { "r " }.to_string(),
1102 )];
1103 let length = self.duration_node(rest.duration())?;
1104 parts.push(Item::Node(length));
1105 let post = post_events(rest.tie().map(|tie| tie.tie_type()), rest.expressions());
1106 self.push_simple(parts, post)?;
1107 self.tuplet_stop(duration);
1108 Ok(())
1109 }
1110
1111 fn push_simple(&mut self, parts: Vec<Item>, post: Vec<String>) -> Result<()> {
1112 let simple = self.tree.add(Kind::SimpleElement { parts });
1113 let chord = self.tree.add(Kind::EventChord {
1114 simple: Some(simple),
1115 post,
1116 note_chord: None,
1117 });
1118 let music = self.tree.add(Kind::SimpleMusic { chord });
1119 self.append_to_context(music)
1120 }
1121
1122 fn append_chord(&mut self, chord: &Chord, duration: Option<&Duration>) -> Result<()> {
1125 self.tuplet_start(duration)?;
1126 self.beam_code(chord.beams())?;
1127 self.stem_code(chord.stem_direction())?;
1128 self.beam_code(chord.beams())?;
1130 self.stem_code(chord.stem_direction())?;
1131 let mut elements = Vec::new();
1132 for note in chord.notes() {
1133 let mut parts = vec![Item::Node(self.pitch_node(note.pitch())?)];
1134 parts.extend(accidental_marks(note.pitch()));
1135 elements.push(self.tree.add(Kind::ChordBodyElement { parts }));
1136 }
1137 let body = self.tree.add(Kind::ChordBody { elements });
1138 let length = self.duration_node(&element_duration(duration))?;
1139 let post = post_events(chord.tie().map(|tie| tie.tie_type()), chord.expressions());
1140 let note_chord = self.tree.add(Kind::NoteChordElement {
1141 body,
1142 duration: length,
1143 post,
1144 });
1145 let event = self.tree.add(Kind::EventChord {
1146 simple: None,
1147 post: Vec::new(),
1148 note_chord: Some(note_chord),
1149 });
1150 let music = self.tree.add(Kind::SimpleMusic { chord: event });
1151 self.append_to_context(music)?;
1152 self.tuplet_stop(duration);
1153 Ok(())
1154 }
1155
1156 fn pitch_node(&mut self, pitch: &Pitch) -> Result<usize> {
1158 let quotes = octave_marks(pitch)?;
1159 Ok(self.tree.add(Kind::Pitch {
1160 name: base_name(pitch),
1161 quotes,
1162 }))
1163 }
1164
1165 fn duration_node(&mut self, duration: &Duration) -> Result<usize> {
1167 let (number, dots) = duration_number(duration)?;
1168 Ok(self.tree.add(Kind::Duration { number, dots }))
1169 }
1170
1171 fn close_measure(&mut self) -> Option<usize> {
1173 let measure = self.current_measure.take()?;
1174 let mut bar = match measure.right_barline() {
1175 None => format!("\\bar {}", quote("|")),
1176 Some(barline) => format!("\\bar {}", quote(barline_sign(barline.bar_type()))),
1177 };
1178 bar += &format!(" %{{ end measure {} %}} ", measure.number());
1179 Some(self.embedded(bar))
1180 }
1181
1182 fn padding(&mut self, measure: &Stream) -> Result<Option<usize>> {
1184 let padding = measure.padding_left();
1185 if padding == 0.0 {
1186 return Ok(None);
1187 }
1188 let bar =
1189 meter_of(measure, self.meter.clone()).map_or(4.0, |meter| meter.bar_quarter_length());
1190 let remaining = bar - padding;
1191 if remaining <= 0.0 {
1192 return Err(lily_error("your first pickup measure is non-existent!"));
1193 }
1194 Ok(Some(self.embedded(format!(
1195 "\\partial 32*{} ",
1196 (remaining * 8.0) as i64
1197 ))))
1198 }
1199}
1200
1201fn meter_of(measure: &Stream, fallback: Option<TimeSignature>) -> Option<TimeSignature> {
1203 measure
1204 .events()
1205 .iter()
1206 .find_map(|event| match event.element() {
1207 StreamElement::TimeSignature(meter) => Some(meter.clone()),
1208 _ => None,
1209 })
1210 .or(fallback)
1211}
1212
1213fn element_duration(duration: Option<&Duration>) -> Duration {
1215 duration.cloned().unwrap_or_else(Duration::quarter)
1216}
1217
1218fn duration_number(duration: &Duration) -> Result<(String, u32)> {
1220 let name = crate::braille::basic::duration_type(duration);
1221 let number: FloatType = match name.as_str() {
1222 "duplex-maxima" => 0.0625,
1223 "maxima" => 0.125,
1224 "longa" => 0.25,
1225 "breve" => 0.5,
1226 "zero" => 0.0,
1227 "inexpressible" | "complex" => {
1228 return Err(lily_error(format!(
1229 "DurationException for durationObject {}: Could not determine durationNumber from {name}",
1230 duration_repr(duration)
1231 )));
1232 }
1233 "whole" => 1.0,
1234 "half" => 2.0,
1235 "quarter" => 4.0,
1236 "eighth" => 8.0,
1237 other => other
1238 .trim_end_matches(|c: char| c.is_ascii_alphabetic())
1239 .parse()
1240 .unwrap_or(0.0),
1241 };
1242 if number == 0.0 {
1243 return Err(lily_error(format!(
1244 "Cannot translate an object of zero duration {}",
1245 duration_repr(duration)
1246 )));
1247 }
1248 let written = if number < 1.0 {
1249 if number == 0.5 {
1250 "\\breve".to_string()
1251 } else if number == 0.25 {
1252 "\\longa".to_string()
1253 } else {
1254 return Err(lily_error("Cannot support durations longer than longa"));
1255 }
1256 } else {
1257 format!("{}", number as i64)
1258 };
1259 Ok((written, crate::braille::basic::duration_dots(duration)))
1260}
1261
1262fn duration_repr(duration: &Duration) -> String {
1263 format!(
1264 "<music21.duration.Duration {}>",
1265 crate::braille::basic::quarter_length_repr(duration.quarter_length())
1266 )
1267}
1268
1269fn base_name(pitch: &Pitch) -> String {
1271 let mut name = pitch.step().as_char().to_ascii_lowercase().to_string();
1272 if let Some(accidental) = pitch.written_accidental() {
1273 name += match accidental.name() {
1274 "double-sharp" => "isis",
1275 "double-flat" => "eses",
1276 "one-and-a-half-sharp" => "isih",
1277 "one-and-a-half-flat" => "eseh",
1278 "sharp" => "is",
1279 "flat" => "es",
1280 "half-sharp" => "ih",
1281 "half-flat" => "eh",
1282 _ => "",
1283 };
1284 }
1285 name
1286}
1287
1288fn octave_marks(pitch: &Pitch) -> Result<String> {
1290 let Some(octave) = pitch.octave() else {
1291 return Err(lily_error(
1292 "a pitch with no octave has no place on the staff",
1293 ));
1294 };
1295 Ok(if octave < 3 {
1296 ",".repeat((3 - octave) as usize)
1297 } else {
1298 "'".repeat((octave - 3) as usize)
1299 })
1300}
1301
1302fn accidental_marks(pitch: &Pitch) -> Vec<Item> {
1304 let mut marks = Vec::new();
1305 if let Some(accidental) = pitch.written_accidental() {
1306 if accidental.display_type() == "always" {
1307 marks.push(Item::Text("! ".to_string()));
1308 }
1309 if accidental.display_style() == "parentheses" {
1310 marks.push(Item::Text("? ".to_string()));
1311 }
1312 }
1313 marks
1314}
1315
1316fn post_events(tie: Option<TieType>, expressions: &[Expression]) -> Vec<String> {
1318 let mut post = Vec::new();
1319 if tie.is_some_and(|tie| tie != TieType::Stop) {
1320 post.push("~ ".to_string());
1321 }
1322 for expression in expressions {
1323 if matches!(expression, Expression::Fermata(_)) {
1324 post.push("\\fermata ".to_string());
1325 }
1326 }
1327 post
1328}
1329
1330fn clef_scheme(clef: &Clef) -> String {
1332 let kind = clef.kind();
1333 let classes: Vec<&str> = std::iter::once(kind.class_name())
1334 .chain(kind.parents().iter().copied())
1335 .collect();
1336 let name = [
1337 ("Treble8vbClef", "treble_8"),
1338 ("Treble8vaClef", "treble^8"),
1339 ("TrebleClef", "treble"),
1340 ("Bass8vbClef", "bass_8"),
1341 ("Bass8vaClef", "bass^8"),
1342 ("BassClef", "bass"),
1343 ("AltoClef", "alto"),
1344 ("TenorClef", "tenor"),
1345 ("SopranoClef", "soprano"),
1346 ("PercussionClef", "percussion"),
1347 ]
1348 .iter()
1349 .find(|(class, _)| classes.contains(class))
1350 .map_or("", |(_, name)| name);
1351 format!("\\clef {}\n", quote(name))
1352}
1353
1354fn key_scheme(key: &Key) -> String {
1361 if !crate::key::MODES.contains(&key.mode()) {
1362 return key_scheme(&key.key_signature().as_key("major"));
1363 }
1364 format!("\\key {} \\{} \n", base_name(&key.tonic()), key.mode())
1365}
1366
1367fn time_scheme(meter: &TimeSignature) -> String {
1369 format!("\\time {}\n", meter.ratio_string())
1370}
1371
1372fn tempo_scheme(mark: &MetronomeMark) -> Result<Option<String>> {
1374 let Some(number) = mark.number() else {
1375 return Ok(None);
1376 };
1377 let (steno, dots) = duration_number(mark.referent())?;
1378 let steno = format!("{steno}{} ", ".".repeat(dots as usize));
1379 Ok(Some(format!(
1380 "\\tempo {steno} = {}\n",
1381 if number.fract() == 0.0 {
1382 format!("{}", number as i64)
1383 } else {
1384 crate::statistics::python_repr(number)
1385 }
1386 )))
1387}
1388
1389fn barline_sign(bar: BarlineType) -> &'static str {
1391 match bar {
1392 BarlineType::Regular => "|",
1393 BarlineType::Dotted => ";",
1394 BarlineType::Dashed => "!",
1395 BarlineType::Heavy => ".",
1396 BarlineType::Double => "||",
1397 BarlineType::Final => "|.",
1398 BarlineType::HeavyLight => ".|",
1399 BarlineType::HeavyHeavy => "..",
1400 BarlineType::Tick => "'",
1401 BarlineType::Short => ",",
1402 BarlineType::None => "",
1403 }
1404}
1405
1406fn staff_lines(stream: &Stream) -> i64 {
1408 for (offset, element) in stream.recurse() {
1409 if offset > 0.0 {
1410 break;
1411 }
1412 if let StreamElement::Layout(crate::layout::Layout::Staff(staff)) = element
1413 && let Some(lines) = staff.staff_lines
1414 {
1415 return lines;
1416 }
1417 }
1418 5
1419}
1420
1421fn stream_lyrics(stream: &Stream) -> Vec<(i64, Vec<Option<&crate::notation::Lyric>>)> {
1424 let mut verses: Vec<(i64, Vec<Option<&crate::notation::Lyric>>)> = Vec::new();
1425 let mut count = 0usize;
1426 let mut notes: Vec<&StreamElement> = Vec::new();
1427 for event in stream.events() {
1428 match event.element() {
1429 StreamElement::Stream(measure) if measure.kind() == StreamKind::Measure => {
1430 notes.extend(
1431 measure
1432 .events()
1433 .iter()
1434 .map(|event| event.element())
1435 .filter(|element| is_not_rest(element)),
1436 );
1437 }
1438 element if is_not_rest(element) => notes.push(element),
1439 _ => {}
1440 }
1441 }
1442 for element in notes {
1443 let tie = match element {
1444 StreamElement::Note(note) => note.tie().map(|tie| tie.tie_type()),
1445 StreamElement::Chord(chord) => chord.tie().map(|tie| tie.tie_type()),
1446 _ => None,
1447 };
1448 if tie.is_some_and(|tie| tie != TieType::Start) {
1449 continue;
1450 }
1451 let lyrics: &[crate::notation::Lyric] = match element {
1452 StreamElement::Note(note) => note.lyrics(),
1453 StreamElement::Chord(chord) => chord.lyrics(),
1454 _ => &[],
1455 };
1456 let mut said: Vec<i64> = Vec::new();
1457 for lyric in lyrics {
1458 let number = lyric.number() as i64;
1459 match verses.iter_mut().find(|(known, _)| *known == number) {
1460 Some((_, list)) => list.push(Some(lyric)),
1461 None => {
1462 let mut list = vec![None; count];
1463 list.push(Some(lyric));
1464 verses.push((number, list));
1465 }
1466 }
1467 said.push(number);
1468 }
1469 for (number, list) in &mut verses {
1470 if !said.contains(number) {
1471 list.push(None);
1472 }
1473 }
1474 count += 1;
1475 }
1476 verses.sort_by_key(|(number, _)| *number);
1477 verses
1478}
1479
1480fn is_not_rest(element: &StreamElement) -> bool {
1482 matches!(
1483 element,
1484 StreamElement::Note(_)
1485 | StreamElement::Chord(_)
1486 | StreamElement::Unpitched(_)
1487 | StreamElement::PercussionChord(_)
1488 | StreamElement::ChordSymbol(_)
1489 )
1490}
1491
1492pub fn to_lilypond(stream: &Stream, options: &LilyOptions) -> Result<String> {
1507 let wrapped;
1508 let score = if stream.kind() == StreamKind::Score {
1509 stream
1510 } else {
1511 let mut score = Stream::with_kind(StreamKind::Score);
1512 if matches!(stream.kind(), StreamKind::Measure | StreamKind::Voice) {
1513 let mut part = Stream::with_kind(StreamKind::Part);
1514 part.insert(0.0, StreamElement::Stream(Box::new(stream.clone())));
1515 score.insert(0.0, StreamElement::Stream(Box::new(part)));
1516 } else {
1517 score.insert(0.0, StreamElement::Stream(Box::new(stream.clone())));
1518 }
1519 wrapped = score;
1520 &wrapped
1521 };
1522 let mut converter = Converter::new();
1523 converter.load_score(score, options)?;
1524 let written = converter.tree.str_of(converter.top);
1525 Ok(collapse_blank_lines(&written).trim().to_string())
1526}
1527
1528fn collapse_blank_lines(text: &str) -> String {
1531 let chars: Vec<char> = text.chars().collect();
1532 let mut out = String::with_capacity(text.len());
1533 let mut index = 0;
1534 while index < chars.len() {
1535 if !chars[index].is_whitespace() {
1536 out.push(chars[index]);
1537 index += 1;
1538 continue;
1539 }
1540 let mut end = index;
1541 while end < chars.len() && chars[end].is_whitespace() {
1542 end += 1;
1543 }
1544 let newlines: Vec<usize> = (index..end).filter(|&at| chars[at] == '\n').collect();
1545 if let [_, .., last] = newlines[..] {
1546 out.push('\n');
1547 out.extend(&chars[last + 1..end]);
1548 } else {
1549 out.extend(&chars[index..end]);
1550 }
1551 index = end;
1552 }
1553 out
1554}
1555
1556#[cfg(test)]
1557mod tests {
1558 use super::*;
1559 use crate::tinynotation::from_tiny_notation;
1560
1561 #[test]
1562 fn blank_lines_are_closed_up_as_python_closes_them() {
1563 assert_eq!(collapse_blank_lines("a \n \n b"), "a\n b");
1564 assert_eq!(collapse_blank_lines("a\n\n\nb"), "a\nb");
1565 assert_eq!(collapse_blank_lines("a\nb"), "a\nb");
1566 }
1567
1568 #[test]
1569 fn ids_are_made_of_letters_as_music21_makes_them() {
1570 assert_eq!(letters_only("P1"), "Px");
1571 assert_eq!(letters_only("Soprano"), "Soprano");
1572 }
1573
1574 #[test]
1575 fn a_line_is_written_as_music21_writes_it() {
1576 let line = from_tiny_notation("4/4 c4 d8 e f#4. g8 a2 r2").unwrap();
1577 let text = to_lilypond(&line, &LilyOptions::default()).unwrap();
1578 assert!(text.contains("\\time 4/4"));
1579 assert!(text.contains("fis' 4."));
1580 assert!(text.contains("\\bar \"|.\" %{ end measure 2 %}"));
1581 }
1582
1583 const SCHEME_CALL: &str = "\\\" #(system \"touch x\") %";
1585 const ESCAPED: &str = "\"\\\\\\\" #(system \\\"touch x\\\") %\"";
1587
1588 fn one_note(edit: impl FnOnce(&mut crate::Note)) -> Stream {
1589 let mut note = crate::Note::from_name("C4").unwrap();
1590 edit(&mut note);
1591 let mut line = Stream::new();
1592 line.push(note);
1593 line
1594 }
1595
1596 #[test]
1597 fn titles_and_lyrics_stay_inside_their_strings() {
1598 let line = one_note(|note| {
1599 note.lyrics_mut()
1600 .push(crate::notation::Lyric::new(SCHEME_CALL));
1601 });
1602 let mut score = Stream::with_kind(StreamKind::Score);
1603 score.push(line);
1604 let mut metadata = crate::metadata::Metadata::new();
1605 metadata.add_text("title", SCHEME_CALL);
1606 score.set_metadata(Some(metadata));
1607 let text = to_lilypond(&score, &LilyOptions::default()).unwrap();
1608 assert!(text.contains(&format!("title = {ESCAPED}")), "{text}");
1609 assert_eq!(text.matches(ESCAPED).count(), 2, "{text}");
1610 }
1611
1612 #[test]
1613 fn a_colour_stays_inside_its_string() {
1614 let line = one_note(|note| note.set_color(Some(SCHEME_CALL.to_string())));
1615 let text = to_lilypond(&line, &LilyOptions::default()).unwrap();
1616 assert!(
1617 text.contains(&format!("\\override NoteHead.color = {ESCAPED}\n")),
1618 "{text}"
1619 );
1620 assert!(
1621 text.contains(&format!("\\override Stem.color = {ESCAPED}\n")),
1622 "{text}"
1623 );
1624 }
1625
1626 #[test]
1627 fn a_key_writes_only_a_mode_lilypond_knows() {
1628 let mut line = Stream::new();
1629 line.push(Key::new(
1630 crate::Pitch::from_name("C").unwrap(),
1631 "major #(system \"touch x\") %",
1632 0,
1633 ));
1634 line.push(crate::Note::from_name("C4").unwrap());
1635 let text = to_lilypond(&line, &LilyOptions::default()).unwrap();
1636 assert!(!text.contains("#(system"), "{text}");
1637 assert!(text.contains("\\key c \\major"), "{text}");
1638
1639 let mut line = Stream::new();
1640 line.push(Key::from_tonic_mode("D", "dorian").unwrap());
1641 line.push(crate::Note::from_name("D4").unwrap());
1642 let text = to_lilypond(&line, &LilyOptions::default()).unwrap();
1643 assert!(text.contains("\\key d \\dorian"), "{text}");
1644 }
1645}