1# This file is part of python-ly, https://pypi.python.org/pypi/python-ly
2#
3# Copyright (c) 2008 - 2015 by Wilbert Berendsen
4#
5# This program is free software; you can redistribute it and/or
6# modify it under the terms of the GNU General Public License
7# as published by the Free Software Foundation; either version 2
8# of the License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with this program; if not, write to the Free Software
17# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18# See http://www.gnu.org/licenses/ for more information.
19
20"""
21Using the tree structure from ly.music to initiate the conversion to MusicXML.
22
23Uses functions similar to ly.music.items.Document.iter_music() to iter through
24the node tree. But information about where a node branch ends
25is also added. During the iteration the information needed for the conversion
26is captured.
27"""
28
29from __future__ import unicode_literals
30from __future__ import print_function
31
32import ly.document
33import ly.music
34
35from . import create_musicxml
36from . import ly2xml_mediator
37from . import xml_objs
38
39#excluded from parsing
40excl_list = ['Version', 'Midi', 'Layout']
41
42
43# Defining contexts in relation to musicXML
44group_contexts = ['StaffGroup', 'ChoirStaff']
45
46pno_contexts = ['PianoStaff', 'GrandStaff']
47
48staff_contexts = ['Staff', 'RhythmicStaff', 'TabStaff',
49    'DrumStaff', 'VaticanaStaff', 'MensuralStaff']
50
51part_contexts = pno_contexts + staff_contexts
52
53
54class End():
55    """ Extra class that gives information about the end of Container
56    elements in the node list. """
57    def __init__(self, node):
58        self.node = node
59
60    def __repr__(self):
61        return '<{0} {1}>'.format(self.__class__.__name__, self.node)
62
63
64class ParseSource():
65    """ creates the XML-file from the source code according to the Music XML standard """
66
67    def __init__(self):
68        self.musxml = create_musicxml.CreateMusicXML()
69        self.mediator = ly2xml_mediator.Mediator()
70        self.relative = False
71        self.tuplet = []
72        self.scale = ''
73        self.grace_seq = False
74        self.trem_rep = 0
75        self.piano_staff = 0
76        self.numericTime = False
77        self.voice_sep = False
78        self.sims_and_seqs = []
79        self.override_dict = {}
80        self.ottava = False
81        self.with_contxt = None
82        self.schm_assignm = None
83        self.tempo = ()
84        self.tremolo = False
85        self.tupl_span = False
86        self.unset_tuplspan = False
87        self.alt_mode = None
88        self.rel_pitch_isset = False
89        self.slurcount = 0
90        self.slurnr = 0
91        self.phrslurnr = 0
92        self.mark = False
93        self.pickup = False
94
95    def parse_text(self, ly_text, filename=None):
96        """Parse the LilyPond source specified as text.
97
98        If you specify a filename, it can be used to resolve \\include commands
99        correctly.
100
101        """
102        doc = ly.document.Document(ly_text)
103        doc.filename = filename
104        self.parse_document(doc)
105
106    def parse_document(self, ly_doc, relative_first_pitch_absolute=False):
107        """Parse the LilyPond source specified as a ly.document document.
108
109        If relative_first_pitch_absolute is set to True, the first pitch in a
110        \relative expression without startpitch is considered to be absolute
111        (LilyPond 2.18+ behaviour).
112
113        """
114        # The document is copied and the copy is converted to absolute mode to
115        # facilitate the export. The original document is unchanged.
116        doc = ly_doc.copy()
117        import ly.pitch.rel2abs
118        cursor = ly.document.Cursor(doc)
119        ly.pitch.rel2abs.rel2abs(cursor, first_pitch_absolute=relative_first_pitch_absolute)
120        mustree = ly.music.document(doc)
121        self.parse_tree(mustree)
122
123    def parse_tree(self, mustree):
124        """Parse the LilyPond source as a ly.music node tree."""
125        # print(mustree.dump())
126        header_nodes = self.iter_header(mustree)
127        if header_nodes:
128            self.parse_nodes(header_nodes)
129        score = self.get_score(mustree)
130        if score:
131            mus_nodes = self.iter_score(score, mustree)
132        else:
133            mus_nodes = self.find_score_sub(mustree)
134        self.mediator.new_section("fallback") #fallback/default section
135        self.parse_nodes(mus_nodes)
136
137    def parse_nodes(self, nodes):
138        """Work through all nodes by calling the function with the
139        same name as the nodes class."""
140        if nodes:
141            for m in nodes:
142                # print(m)
143                func_name = m.__class__.__name__ #get instance name
144                if func_name not in excl_list:
145                    try:
146                        func_call = getattr(self, func_name)
147                        func_call(m)
148                    except AttributeError as ae:
149                        print("Warning:", func_name, "not implemented!")
150                        print(ae)
151                        pass
152        else:
153            print("Warning! Couldn't parse source!")
154
155    def musicxml(self, prettyprint=True):
156        self.mediator.check_score()
157        xml_objs.IterateXmlObjs(
158            self.mediator.score, self.musxml, self.mediator.divisions)
159        xml = self.musxml.musicxml(prettyprint)
160        return xml
161
162    ##
163    # The different source types from ly.music are here sent to translation.
164    ##
165
166    def Assignment(self, a):
167        """
168        Variables should already have been substituted
169        so this need only cover other types of assignments.
170        """
171        if isinstance(a.value(), ly.music.items.Markup):
172            val = a.value().plaintext()
173        elif isinstance(a.value(), ly.music.items.String):
174            val = a.value().value()
175        elif isinstance(a.value(), ly.music.items.Scheme):
176            val = a.value().get_string()
177            if not val:
178                self.schm_assignm = a.name()
179        elif isinstance(a.value(), ly.music.items.UserCommand):
180            # Don't know what to do with this:
181            return
182        if self.look_behind(a, ly.music.items.With):
183            if self.with_contxt in group_contexts:
184                self.mediator.set_by_property(a.name(), val, True)
185            else:
186                self.mediator.set_by_property(a.name(), val)
187        else:
188            self.mediator.new_header_assignment(a.name(), val)
189
190    def MusicList(self, musicList):
191        if musicList.token == '<<':
192            if self.look_ahead(musicList, ly.music.items.VoiceSeparator):
193                self.mediator.new_snippet('sim-snip')
194                self.voice_sep = True
195            else:
196                self.mediator.new_section('simultan')
197                self.sims_and_seqs.append('sim')
198        elif musicList.token == '{':
199            self.sims_and_seqs.append('seq')
200
201    def Chord(self, chord):
202        self.mediator.clear_chord()
203
204    def Q(self, q):
205        self.mediator.copy_prev_chord(q.duration)
206
207    def Context(self, context):
208        r""" \context """
209        self.in_context = True
210        self.check_context(context.context(), context.context_id(), context.token)
211
212    def check_context(self, context, context_id=None, token=""):
213        """Check context and do appropriate action (e.g. create new part)."""
214        # Check first if part already exists
215        if context_id:
216            match = self.mediator.get_part_by_id(context_id)
217            if match:
218                self.mediator.new_part(to_part=match)
219                return
220        if context in pno_contexts:
221            self.mediator.new_part(context_id, piano=True)
222            self.piano_staff = 1
223        elif context in group_contexts:
224            self.mediator.new_group()
225        elif context in staff_contexts:
226            if self.piano_staff:
227                if self.piano_staff > 1:
228                    self.mediator.set_voicenr(nr=self.piano_staff+3)
229                self.mediator.new_section('piano-staff'+str(self.piano_staff))
230                self.mediator.set_staffnr(self.piano_staff)
231                self.piano_staff += 1
232            else:
233                if token != '\\context' or self.mediator.part_not_empty():
234                    self.mediator.new_part(context_id)
235            self.mediator.add_staff_id(context_id)
236        elif context == 'Voice':
237            self.sims_and_seqs.append('voice')
238            if context_id:
239                self.mediator.new_section(context_id)
240            else:
241                self.mediator.new_section('voice')
242        elif context == 'Devnull':
243            self.mediator.new_section('devnull', True)
244        else:
245            print("Context not implemented:", context)
246
247    def VoiceSeparator(self, voice_sep):
248        self.mediator.new_snippet('sim')
249        self.mediator.set_voicenr(add=True)
250
251    def Change(self, change):
252        r""" A \change music expression. Changes the staff number. """
253        if change.context() == 'Staff':
254            self.mediator.set_staffnr(0, staff_id=change.context_id())
255
256    def PipeSymbol(self, barcheck):
257        """ PipeSymbol = | """
258        pass
259
260    def Clef(self, clef):
261        r""" Clef \clef"""
262        self.mediator.new_clef(clef.specifier())
263
264    def KeySignature(self, key):
265        self.mediator.new_key(key.pitch().output(), key.mode())
266
267    def Relative(self, relative):
268        r"""A \relative music expression."""
269        self.relative = True
270
271    def Partial(self, partial):
272        self.pickup = True
273
274    def Note(self, note):
275        """ notename, e.g. c, cis, a bes ... """
276        #print(note.token)
277        if note.length():
278            if self.relative and not self.rel_pitch_isset:
279                self.mediator.new_note(note, False)
280                self.mediator.set_relative(note)
281                self.rel_pitch_isset = True
282            else:
283                self.mediator.new_note(note, self.relative)
284            self.check_note(note)
285        else:
286            if isinstance(note.parent(), ly.music.items.Relative):
287                self.mediator.set_relative(note)
288                self.rel_pitch_isset = True
289            elif isinstance(note.parent(), ly.music.items.Chord):
290                if self.mediator.current_chord:
291                    self.mediator.new_chord(note, chord_base=False)
292                else:
293                    self.mediator.new_chord(note, note.parent().duration, self.relative)
294                    self.check_tuplet()
295                # chord as grace note
296                if self.grace_seq:
297                    self.mediator.new_chord_grace()
298
299    def Unpitched(self, unpitched):
300        """A note without pitch, just a standalone duration."""
301        if unpitched.length():
302            if self.alt_mode == 'drum':
303                self.mediator.new_iso_dura(unpitched, self.relative, True)
304            else:
305                self.mediator.new_iso_dura(unpitched, self.relative)
306            self.check_note(unpitched)
307
308    def DrumNote(self, drumnote):
309        """A note in DrumMode."""
310        if drumnote.length():
311            self.mediator.new_note(drumnote, is_unpitched=True)
312            self.check_note(drumnote)
313
314    def check_note(self, note):
315        """Generic check for all notes, both pitched and unpitched."""
316        self.check_tuplet()
317        if self.grace_seq:
318            self.mediator.new_grace()
319        if self.trem_rep and not self.look_ahead(note, ly.music.items.Duration):
320            self.mediator.set_tremolo(trem_type='start', repeats=self.trem_rep)
321
322    def check_tuplet(self):
323        """Generic tuplet check."""
324        if self.tuplet:
325            tlevels = len(self.tuplet)
326            nested = True if tlevels > 1 else False
327            for td in self.tuplet:
328                if nested:
329                    self.mediator.change_to_tuplet(td['fraction'], td['ttype'],
330                                                td['nr'], td['length'])
331                else:
332                    self.mediator.change_to_tuplet(td['fraction'], td['ttype'],
333                                                td['nr'])
334                td['ttype'] = ""
335            self.mediator.check_divs()
336
337    def Duration(self, duration):
338        """A written duration"""
339        if self.tempo:
340            self.mediator.new_tempo(duration.token, duration.tokens, *self.tempo)
341            self.tempo = ()
342        elif self.tremolo:
343            self.mediator.set_tremolo(duration=int(duration.token))
344            self.tremolo = False
345        elif self.tupl_span:
346            self.mediator.set_tuplspan_dur(duration.token, duration.tokens)
347            self.tupl_span = False
348        elif self.pickup:
349            self.mediator.set_pickup()
350            self.pickup = False
351        else:
352            self.mediator.new_duration_token(duration.token, duration.tokens)
353            if self.trem_rep:
354                self.mediator.set_tremolo(trem_type='start', repeats=self.trem_rep)
355
356    def Tempo(self, tempo):
357        """ Tempo direction, e g '4 = 80' """
358        if self.look_ahead(tempo, ly.music.items.Duration):
359            self.tempo = (tempo.tempo(), tempo.text())
360        else:
361            self.mediator.new_tempo(0, (), tempo.tempo(), tempo.text())
362
363    def Tie(self, tie):
364        """ tie ~ """
365        self.mediator.tie_to_next()
366
367    def Rest(self, rest):
368        r""" rest, r or R. Note: NOT by command, i.e. \rest """
369        if rest.token == 'R':
370            self.scale = 'R'
371        self.mediator.new_rest(rest)
372
373    def Skip(self, skip):
374        r""" invisible rest/spacer rest (s or command \skip)"""
375        if 'lyrics' in self.sims_and_seqs:
376            self.mediator.new_lyrics_item(skip.token)
377        else:
378            self.mediator.new_rest(skip)
379
380    def Scaler(self, scaler):
381        r"""
382        \times \tuplet \scaleDurations
383
384        """
385        if scaler.token == '\\scaleDurations':
386            ttype = ""
387            fraction = (scaler.denominator, scaler.numerator)
388        elif scaler.token == '\\times':
389            ttype = "start"
390            fraction = (scaler.denominator, scaler.numerator)
391        elif scaler.token == '\\tuplet':
392            ttype = "start"
393            fraction = (scaler.numerator, scaler.denominator)
394        nr = len(self.tuplet) + 1
395        self.tuplet.append({'set': False,
396                            'fraction': fraction,
397                            'ttype': ttype,
398                            'length': scaler.length(),
399                            'nr': nr})
400        if self.look_ahead(scaler, ly.music.items.Duration):
401            self.tupl_span = True
402            self.unset_tuplspan = True
403
404    def Number(self, number):
405        pass
406
407    def Articulation(self, art):
408        """An articulation, fingering, string number, or other symbol."""
409        self.mediator.new_articulation(art.token)
410
411    def Postfix(self, postfix):
412        pass
413
414    def Beam(self, beam):
415        pass
416
417    def Slur(self, slur):
418        """ Slur, '(' = start, ')' = stop. """
419        if slur.token == '(':
420            self.slurcount += 1
421            self.slurnr = self.slurcount
422            self.mediator.set_slur(self.slurnr, "start")
423        elif slur.token == ')':
424            self.mediator.set_slur(self.slurnr, "stop")
425            self.slurcount -= 1
426
427    def PhrasingSlur(self, phrslur):
428        r"""A \( or \)."""
429        if phrslur.token == '\(':
430            self.slurcount += 1
431            self.phrslurnr = self.slurcount
432            self.mediator.set_slur(self.phrslurnr, "start", phrasing=True)
433        elif phrslur.token == '\)':
434            self.mediator.set_slur(self.phrslurnr, "stop", phrasing=True)
435            self.slurcount -= 1
436
437    def Dynamic(self, dynamic):
438        """Any dynamic symbol."""
439        self.mediator.new_dynamics(dynamic.token[1:])
440
441    def Grace(self, grace):
442        self.grace_seq = True
443
444    def TimeSignature(self, timeSign):
445        self.mediator.new_time(timeSign.numerator(), timeSign.fraction(), self.numericTime)
446
447    def Repeat(self, repeat):
448        if repeat.specifier() == 'volta':
449            self.mediator.new_repeat('forward')
450        elif repeat.specifier() == 'tremolo':
451            self.trem_rep = repeat.repeat_count()
452
453    def Tremolo(self, tremolo):
454        """A tremolo item ":"."""
455        if self.look_ahead(tremolo, ly.music.items.Duration):
456            self.tremolo = True
457        else:
458            self.mediator.set_tremolo()
459
460    def With(self, cont_with):
461        r"""A \with ... construct."""
462        self.with_contxt = cont_with.parent().context()
463
464    def Set(self, cont_set):
465        r"""A \set command."""
466        if isinstance(cont_set.value(), ly.music.items.Scheme):
467            if cont_set.property() == 'tupletSpannerDuration':
468                moment = cont_set.value().get_ly_make_moment()
469                if moment:
470                    self.mediator.set_tuplspan_dur(fraction=moment)
471                else:
472                    self.mediator.unset_tuplspan_dur()
473                return
474            val = cont_set.value().get_string()
475        else:
476            val = cont_set.value().value()
477        if cont_set.context() in part_contexts:
478            self.mediator.set_by_property(cont_set.property(), val)
479        elif cont_set.context() in group_contexts:
480            self.mediator.set_by_property(cont_set.property(), val, group=True)
481
482    def Command(self, command):
483        r""" \bar, \rest etc """
484        excls = ['\\major', '\\minor', '\\dorian', '\\bar']
485        if command.token == '\\rest':
486            self.mediator.note2rest()
487        elif command.token == '\\numericTimeSignature':
488            self.numericTime = True
489        elif command.token == '\\defaultTimeSignature':
490            self.numericTime = False
491        elif command.token.find('voice') == 1:
492            self.mediator.set_voicenr(command.token[1:], piano=self.piano_staff)
493        elif command.token == '\\glissando':
494            try:
495                self.mediator.new_gliss(self.override_dict["Glissando.style"])
496            except KeyError:
497                self.mediator.new_gliss()
498        elif command.token == '\\startTrillSpan':
499            self.mediator.new_trill_spanner()
500        elif command.token == '\\stopTrillSpan':
501            self.mediator.new_trill_spanner("stop")
502        elif command.token == '\\ottava':
503            self.ottava = True
504        elif command.token == '\\mark':
505            self.mark = True
506            self.mediator.new_mark()
507        elif command.token == '\\breathe':
508            art = type('',(object,),{"token": "\\breathe"})()
509            self.Articulation(art)
510        elif command.token == '\\stemUp' or command.token == '\\stemDown' or command.token == '\\stemNeutral':
511            self.mediator.stem_direction(command.token)
512        elif command.token == '\\default':
513            if self.tupl_span:
514                self.mediator.unset_tuplspan_dur()
515                self.tupl_span = False
516            elif self.mark:
517                self.mark = False
518        elif command.token == '\\compressFullBarRests':
519            self.mediator.set_mult_rest()
520        elif command.token == '\\break':
521            self.mediator.add_break()
522        else:
523            if command.token not in excls:
524                print("Unknown command:", command.token)
525
526    def UserCommand(self, usercommand):
527        """Music variables are substituted so this must be something else."""
528        if usercommand.name() == 'tupletSpan':
529            self.tupl_span = True
530
531    def Markup(self, markup):
532        pass
533
534    def MarkupWord(self, markupWord):
535        self.mediator.new_word(markupWord.token)
536
537    def MarkupList(self, markuplist):
538        pass
539
540    def String(self, string):
541        prev = self.get_previous_node(string)
542        if prev and prev.token == '\\bar':
543            self.mediator.create_barline(string.value())
544
545    def LyricsTo(self, lyrics_to):
546        r"""A \lyricsto expression. """
547        self.mediator.new_lyric_section('lyricsto'+lyrics_to.context_id(), lyrics_to.context_id())
548        self.sims_and_seqs.append('lyrics')
549
550    def LyricText(self, lyrics_text):
551        """A lyric text (word, markup or string), with a Duration."""
552        self.mediator.new_lyrics_text(lyrics_text.token)
553
554    def LyricItem(self, lyrics_item):
555        """Another lyric item (skip, extender, hyphen or tie)."""
556        self.mediator.new_lyrics_item(lyrics_item.token)
557
558    def NoteMode(self, notemode):
559        r"""A \notemode or \notes expression."""
560        self.alt_mode = 'note'
561
562    def ChordMode(self, chordmode):
563        r"""A \chordmode or \chords expression."""
564        self.alt_mode = 'chord'
565
566    def DrumMode(self, drummode):
567        r"""A \drummode or \drums expression.
568
569        If the shorthand form \drums is found, DrumStaff is implicit.
570
571        """
572        if drummode.token == '\\drums':
573            self.check_context('DrumStaff')
574        self.alt_mode = 'drum'
575
576    def FigureMode(self, figmode):
577        r"""A \figuremode or \figures expression."""
578        self.alt_mode = 'figure'
579
580    def LyricMode(self, lyricmode):
581        r"""A \lyricmode, \lyrics or \addlyrics expression."""
582        self.alt_mode = 'lyric'
583
584    def Override(self, override):
585        r"""An \override command."""
586        self.override_key = ''
587
588    def PathItem(self, item):
589        r"""An item in the path of an \override or \revert command."""
590        self.override_key += item.token
591
592    def Scheme(self, scheme):
593        """A Scheme expression inside LilyPond."""
594        pass
595
596    def SchemeItem(self, item):
597        """Any scheme token."""
598        if self.ottava:
599            self.mediator.new_ottava(item.token)
600            self.ottava = False
601        elif self.look_behind(item, ly.music.items.Override):
602            self.override_dict[self.override_key] = item.token
603        elif self.schm_assignm:
604            self.mediator.set_by_property(self.schm_assignm, item.token)
605        elif self.mark:
606            self.mediator.new_mark(int(item.token))
607        else:
608            print("SchemeItem not implemented:", item.token)
609
610    def SchemeQuote(self, quote):
611        """A ' in scheme."""
612        pass
613
614    def End(self, end):
615        if isinstance(end.node, ly.music.items.Scaler):
616            if self.unset_tuplspan:
617                self.mediator.unset_tuplspan_dur()
618                self.unset_tuplspan = False
619            if end.node.token != '\\scaleDurations':
620                self.mediator.change_tuplet_type(len(self.tuplet) - 1, "stop")
621            self.tuplet.pop()
622            self.fraction = None
623        elif isinstance(end.node, ly.music.items.Grace): #Grace
624            self.grace_seq = False
625        elif end.node.token == '\\repeat':
626            if end.node.specifier() == 'volta':
627                self.mediator.new_repeat('backward')
628            elif end.node.specifier() == 'tremolo':
629                if self.look_ahead(end.node, ly.music.items.MusicList):
630                    self.mediator.set_tremolo(trem_type="stop")
631                else:
632                    self.mediator.set_tremolo(trem_type="single")
633                self.trem_rep = 0
634        elif isinstance(end.node, ly.music.items.Context):
635            self.in_context = False
636            if end.node.context() == 'Voice':
637                self.mediator.check_voices()
638                self.sims_and_seqs.pop()
639            elif end.node.context() in group_contexts:
640                self.mediator.close_group()
641            elif end.node.context() in staff_contexts:
642                if not self.piano_staff:
643                    self.mediator.check_part()
644            elif end.node.context() in pno_contexts:
645                self.mediator.check_voices()
646                self.mediator.check_part()
647                self.piano_staff = 0
648                self.mediator.set_voicenr(nr=1)
649            elif end.node.context() == 'Devnull':
650                self.mediator.check_voices()
651        elif end.node.token == '<<':
652            if self.voice_sep:
653                self.mediator.check_voices_by_nr()
654                self.mediator.revert_voicenr()
655                self.voice_sep = False
656            elif not self.piano_staff:
657                self.mediator.check_simultan()
658                if self.sims_and_seqs:
659                    self.sims_and_seqs.pop()
660        elif end.node.token == '{':
661            if self.sims_and_seqs:
662                self.sims_and_seqs.pop()
663        elif end.node.token == '<': #chord
664            self.mediator.chord_end()
665        elif end.node.token == '\\lyricsto':
666            self.mediator.check_lyrics(end.node.context_id())
667            self.sims_and_seqs.pop()
668        elif end.node.token == '\\with':
669            self.with_contxt = None
670        elif end.node.token == '\\drums':
671            self.mediator.check_part()
672        elif isinstance(end.node, ly.music.items.Relative):
673            self.relative = False
674            self.rel_pitch_isset = False
675        else:
676            # print("end:", end.node.token)
677            pass
678
679    ##
680    # Additional node manipulation
681    ##
682
683    def get_previous_node(self, node):
684        """ Returns the nodes previous node
685        or false if the node is first in its branch. """
686        parent = node.parent()
687        i = parent.index(node)
688        if i > 0:
689            return parent[i-1]
690        else:
691            return False
692
693    def simple_node_gen(self, node):
694        """Unlike iter_score are the subnodes yielded without substitution."""
695        for n in node:
696            yield n
697            for s in self.simple_node_gen(n):
698                yield s
699
700    def iter_header(self, tree):
701        """Iter only over header nodes."""
702        for t in tree:
703            if isinstance(t, ly.music.items.Header):
704                return self.simple_node_gen(t)
705
706    def get_score(self, node):
707        """ Returns (first) Score node or false if no Score is found. """
708        for n in node:
709            if isinstance(n, ly.music.items.Score) or isinstance(n, ly.music.items.Book):
710                return n
711        return False
712
713    def iter_score(self, scorenode, doc):
714        r"""
715        Iter over score.
716
717        Similarly to items.Document.iter_music user commands are substituted.
718
719        Furthermore \repeat unfold expressions are unfolded.
720        """
721        for s in scorenode:
722            if isinstance(s, ly.music.items.Repeat) and s.specifier() == 'unfold':
723                for u in self.unfold_repeat(s, s.repeat_count(), doc):
724                    yield u
725            else:
726                n = doc.substitute_for_node(s) or s
727                yield n
728                for c in self.iter_score(n, doc):
729                    yield c
730                if isinstance(s, ly.music.items.Container):
731                    yield End(s)
732
733    def unfold_repeat(self, repeat_node, repeat_count, doc):
734        r"""
735        Iter over node which represent a \repeat unfold expression
736        and do the unfolding directly.
737        """
738        for r in range(repeat_count):
739            for n in repeat_node:
740                for c in self.iter_score(n, doc):
741                    yield c
742
743    def find_score_sub(self, doc):
744        """Find substitute for scorenode. Takes first music node that isn't
745        an assignment."""
746        for n in doc:
747            if not isinstance(n, ly.music.items.Assignment):
748                if isinstance(n, ly.music.items.Music):
749                    return self.iter_score(n, doc)
750
751    def look_ahead(self, node, find_node):
752        """Looks ahead in a container node and returns True
753        if the search is successful."""
754        for n in node:
755            if isinstance(n, find_node):
756                return True
757        return False
758
759    def look_behind(self, node, find_node):
760        """Looks behind on the parent node(s) and returns True
761        if the search is successful."""
762        parent = node.parent()
763        if parent:
764            if isinstance(parent, find_node):
765                ret = True
766            else:
767                ret = self.look_behind(parent, find_node)
768            return ret
769        else:
770            return False
771
772    ##
773    # Other functions
774    ##
775    def gen_med_caller(self, func_name, *args):
776        """Call any function in the mediator object."""
777        func_call = getattr(self.mediator, func_name)
778        func_call(*args)
779