Source code for quickly.lang.lilypond

# -*- coding: utf-8 -*-
#
# This file is part of `quickly`, a library for LilyPond and the `.ly` format
#
# Copyright © 2019-2020 by Wilbert Berendsen <info@wilbertberendsen.nl>
#
# This module is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.


"""
LilyPond language and transform definition.

The LilyPond language definitions inherits from parce's one, just like the
Scheme language definition in the :mod:`.scheme` module.

A LilyPondTransform is able to transform the parce tree to a quickly.dom
document. Many contexts are transformed in corresponding nodes. Musical
contents are transformed by the :meth:`LilyPondTransform.create_music` method,
that delegates the work to a :class:`MusicBuilder`, which first creates a flat
stream of nodes. Only nodes within music events are already combined, such as
notes, chords and their durations, directions, articulations, tags and tweaks,
etc.

Then music commands are combined with their arguments by looking at the
signatures many Element types provide, which results in a sensible node tree
representing the LilyPond source document. This is done by the
:func:`quickly.dom.element.build_tree` function.

This ``quickly.dom`` tree can then be queried and modified at will.

"""

import itertools

from parce import lexicon
import parce.lang.lilypond
import parce.action as a
from parce.rule import ifarg, bygroup
from parce.util import Dispatcher
from quickly.dom import base, element, htm, lily, scm, tex, util


[docs]class LilyPond(parce.lang.lilypond.LilyPond): """LilyPond language definition."""
[docs] @classmethod def get_scheme_target(cls): """Get *our* Scheme.""" from .scheme import Scheme return Scheme.scheme
@lexicon def html_lilypond_tag(cls): """LilyPond from inside Html, contents of a <lilypond> </lilypond> tag.""" yield ifarg(r'/>'), a.Delimiter, -1 # short single tag version yield ifarg(None, r'(<\s*/)\s*(lilypond)\s*(>)'), bygroup(a.Delimiter, a.Name.Tag, a.Delimiter), -1 yield from cls.root @lexicon def latex_lilypond_environment(cls): yield ifarg(r'\}'), a.Delimiter.Brace, -1 # short \lilypond{ ... } version yield ifarg(None, r'(\\end)(?:\s*(\{)(.*?)(\})|(?=[\W\d]))'), \ bygroup(a.Name.Builtin, a.Delimiter, a.Name.Tag, a.Delimiter), -1 yield from cls.root
[docs]class LilyPondTransform(base.Transform): """Transform LilyPond to Music.""" ## when implementing the transform, keep in mind that the parce transformer ## caches the result of the transform methods. If you return element nodes, ## they are combined into other nodes and thus reparented, which is no ## problem. But you should make sure you do not reparent child nodes of ## nodes that are returned by transform methods. ## A reparented child node is not removed from the cached node, which would ## corrupt the node's structure and cause problems when reusing the node. ## If you decide to use a child node from a node that's returned by ## a transform method; make sure you copy it first, using ## Element.copy_with_origin(). ## transforming methods
[docs] def root(self, items): """Concatenate all nodes in a Document object.""" return lily.Document(*self.handle_assignments(self.create_music(items)))
[docs] def book(self, items): """A Book or BookPart element.""" element_class = lily.BookPart if items[0] == r'\bookpart' else lily.Book return self.create_block(element_class, items, music=True)
[docs] def score(self, items): """A Score element.""" tail = (items.pop(),) if items[-1] == '}' else () head = items[:2] return self.create_block(lily.Score, items, music=True)
[docs] def header(self, items): """A Header element.""" return self.create_block(lily.Header, items, assignments=True)
[docs] def paper(self, items): """A Paper element.""" return self.create_block(lily.Paper, items, assignments=True)
[docs] def layout(self, items): """A Layout element.""" return self.create_block(lily.Layout, items, music=True, assignments=True)
[docs] def midi(self, items): """A Midi element.""" return self.create_block(lily.Midi, items, music=True, assignments=True)
[docs] def layout_context(self, items): """A With or LayoutContext element.""" element_class = lily.With if items[0] == r'\with' else lily.LayoutContext return self.create_block(element_class, items, music=True, assignments=True)
[docs] def musiclist(self, items): """A MusicList (``{`` ... ``}``) or SimultaneousMusicList (``<<`` ... ``>>``) element.""" head = items[:1] tail = (items.pop(),) if items[-1] in ('}', '>>') else () element_class = lily.MusicList if items[0] == '{' else lily.SimultaneousMusicList return self.factory(element_class, head, tail, *self.create_music(items[1:]))
[docs] def chord(self, items): """A Chord element (``<`` ... ``>``).""" head = items[:1] tail = (items.pop(),) if items[-1] == '>' else () body = self.factory(lily.ChordBody, head, tail, *self.create_music(items[1:])) return lily.Chord(body)
[docs] def repeat(self, items): """A list of elements, contents of ``repeat`` context.""" return list(self.common(items))
[docs] def script(self, items): """A Fingering or Articulation event.""" if items[0].action is a.Literal.Number.Fingering: return self.factory(lily.Fingering, items) return self.factory(lily.Articulation, items)
[docs] def pitch(self, items): """A list of elements, Octave, Accidental and/or OctCheck after a note name.""" # can only be Octave, Accidental, OctCheck token or comment element return [self._pitch(i.action, i) if i.is_token else i.obj for i in items]
[docs] def duration(self, items): """A tuple (dots, scaling). ``dots`` is a list of Dot tokens and ``scaling`` is a DurationScaling node or None. """ dots = [] scaling = None for i in items: if i == '.': dots.append(i) elif not i.is_token and i.name == 'duration_scaling': scaling = i.obj return dots, scaling
[docs] def duration_scaling(self, items): """A DurationScaling element (scaling after a duration).""" return self.factory(lily.DurationScaling, items)
[docs] def lyricmode(self, items): """A list of elements, contents of ``lyricmode`` context.""" return list(self.create_music(items))
[docs] def lyricsto(self, items): """A list of elements, contents of ``lyricsto`` context.""" return list(self.common(items))
[docs] def lyriclist(self, items): """A MusicList or SimultaneousMusicList element, the ``{`` ... ``}`` or ``<<`` ... ``>>`` construct in lyricmode.""" node = self.musiclist(items) node[:] = self.postprocess_lyriclist(node) return node
lyricword = None # never creates a context, only used in using()
[docs] def drummode(self, items): """A list of elements, contents of ``drummode`` context.""" return list(self.create_music(items))
[docs] def drumlist(self, items): """A MusicList or SimultaneousMusicList element, the ``{`` ... ``}`` or ``<<`` ... ``>>`` construct in drummode.""" return self.musiclist(items)
[docs] def chordmode(self, items): """A list of elements, contents of ``chordmode`` context.""" return list(self.create_music(items))
[docs] def chordlist(self, items): """A MusicList or SimultaneousMusicList element, the ``{`` ... ``}`` or ``<<`` ... ``>>`` construct in chordmode.""" return self.musiclist(items)
[docs] def chord_modifier(self, items): """A list of elements, contents of ``chord_modifier`` context.""" items = iter(items) nodes = [] for i in items: if i.is_token: if i.action is a.Name.Symbol: # a modifier, such as 'maj' nodes.append(self.factory(lily.Qualifier, (i,))) elif i.action is a.Number: step = self.factory(lily.Step, (i,)) if i.group == 0: alteration = self.factory(lily.Alteration, (next(items),)) step.append(alteration) nodes.append(step) elif i.action is a.Separator.Dot: nodes.append(self.factory(lily.Separator, (i,))) else: nodes.append(i.obj) # can only be comment return nodes
[docs] def notemode(self, items): """A list with elements, contents of ``notemode`` context.""" return list(self.create_music(items))
[docs] def figuremode(self, items): """A list with elements, contents of ``figuremode`` context.""" return list(self.create_music(items))
[docs] def figurelist(self, items): """A MusicList or SimultaneousMusicList element, the ``{`` ... ``}`` construct in figuremode.""" return self.musiclist(items)
[docs] def figure(self, items): """A Figure element.""" head = items[:1] tail = (items.pop(),) if items[-1] == '>' else () body = self.factory(lily.FigureBody, head, tail, *self.create_figure(items)) return lily.Figure(body)
[docs] def figurebracket(self, items): """A FigureBracket element.""" head = items[:1] tail = (items.pop(),) if items[-1] == ']' else () return self.factory(lily.FigureBracket, head, tail, *self.create_figure(items))
def _list_nodes(self, items): """Yield element nodes for List, Identifier or IdentifierRef.""" for i in items: if i.is_token: yield self._id(i.action, i) # String, Int, Symbol, Separator else: yield i.obj # can be a Scheme or String
[docs] def list(self, items): """A List, String, Int, Symbol, or Scheme element.""" nodes = list(self._list_nodes(items)) return nodes[0] if len(nodes) == 1 else lily.List(*nodes)
start_list = None # lexicon never creates tokens
[docs] def identifier_ref(self, items): """An IdentifierRef element.""" node = self.factory(lily.IdentifierRef, items[:1]) node.extend(self._list_nodes(items[1:])) return node
[docs] def markup(self, items): """A list of flattened markup contents, the markup will be constructed later.""" result = [] for i in items: if i.is_token or i.name != "markup": result.append(i) else: result.extend(i.obj) return result
[docs] def markuplist(self, items): """A MarkupList element.""" head = items[:1] tail = (items.pop(),) if items[-1] == '}' else () return self.factory(lily.MarkupList, head, tail, *self.read_markup_arguments(items[1:]))
[docs] def markupscore(self, items): """A MarkupScore element.""" cls = lily.MarkupScoreLines if items[0] == r'\score-lines' else lily.MarkupScore return self.create_block(cls, items, music=True)
[docs] def schemelily(self, items): """A scm.LilyPond element, with LilyPond in Scheme.""" head = items[:1] tail = (items.pop(),) if items[-1] == '#}' else () return self.factory(scm.LilyPond, head, tail, *self.create_music(items[1:]))
[docs] def string(self, items): """A String element.""" return self.factory(lily.String, items)
[docs] def multiline_comment(self, items): """A MultilineComment element.""" return self.factory(lily.MultilineComment, items)
[docs] def singleline_comment(self, items): """A SinglelineComment element.""" return self.factory(lily.SinglelineComment, items)
## LilyPond in HTML
[docs] def html_lilypond_tag(self, items): """Contents of a LilyPond tag. There are two forms of LilyPond input in lilypond-book Html: * within the (self-closing) tag itself, after a colon * between ``<lilypond>`` ... ``</lilypond>`` tags. There may or may not be attributes. Returns a two-tuple: ``([list of elements], tail_origin)``. """ if items: if items[-1] == '/>': tail_origin = (items.pop(),) close_tag = None elif items.peek(-3, a.Delimiter, a.Name.Tag, a.Delimiter): tail_origin = () close_tag = self.factory(htm.CloseTag, items[-3:-2], items[-1:], self.factory(htm.TagName, items[-2:-1])) del items[-3:] nodes = [lily.Document(*self.handle_assignments(self.create_music(items)))] if close_tag: nodes.append(close_tag) return nodes, tail_origin
## LilyPond in Latex
[docs] def latex_lilypond_environment(self, items): r"""Contents of a ``lilypond`` command or environment. Returns a three-tuple(options, contents, tail_origin). The first is the options that must be added to the environment opening command, if any; the second is the contents of the Environment node; and the third is the tail origin of the last brace in the short, ``\lilypond{ ... }`` form. """ opts = [] if items.peek(0, "option"): opts.extend(items[0].obj[0]) items = items[1:] end = None tail_origin = () if items.peek(-4, a.Name.Builtin, a.Delimiter, a.Name.Tag, a.Delimiter): end = self.factory(tex.Command, items[-4:-3]) end.append(self.factory(tex.EnvironmentName, items[-3:])) items = items[:-4] elif items[-1] == '}' and items[-1].action is a.Delimiter.Brace: # short form \lilypond { ... } tail_origin = (items.pop(),) nodes = [lily.Document(*self.handle_assignments(self.create_music(items)))] if end: nodes.append(end) return opts, nodes, tail_origin
## helper methods
[docs] def common(self, items): """Find comment, string, scheme, markup and common tokens. Yields Element objects. """ items = iter(items) for i in items: if i.is_token: result = self._action(i.action, i) if result: yield result elif isinstance(i.obj, element.Element): yield i.obj elif i.name == "markup": yield from self.create_markup(i.obj, items)
[docs] def handle_assignments(self, nodes): """Handle assignments that occur in the Element nodes. If a List, Symbol or String is encountered followed by an EqualSign and a third node, it is turned into an Assignment. Needed at toplevel and in blocks that can contain variable assignments, such as layout, header and paper. """ nodes = iter(nodes) for n in nodes: if isinstance(n, (lily.List, lily.Symbol, lily.String)): variable = [n] equalsign = False for n in nodes: variable.append(n) if not isinstance(n, base.Comment): if equalsign: # gotcha!! first, *rest = variable if isinstance(first, lily.List): identifier = lily.Identifier(*first) else: identifier = lily.Identifier(first) yield lily.Assignment(identifier, *rest) break elif isinstance(n, lily.EqualSign): equalsign = True else: yield from variable break else: yield from variable else: yield n
[docs] def create_block(self, element_class, items, *, music=False, assignments=False): r"""Return a block element for the items. ``element_class`` is the type, e.g. ``lily.Score``; ``items`` are the contents. If ``music`` is set to True, the items are read as music, otherwise as common LilyPond syntax (e.g. enough for a header block). If ``assignments`` is set to True, assignments (list context followed by an equalsign) are converted into Assignment elements. """ tail_origin = (items.pop(),) if items[-1] == '}' else () head_origin = items[:2] if music: items = self.create_music(items[2:]) else: items = element.build_tree(self.common(items[2:]), ignore_type=base.Comment) if assignments: items = self.handle_assignments(items) return self.factory(element_class, head_origin, tail_origin, *items)
[docs] def create_markup(self, markup, items): """Yield zero or one Markup element. ``markup`` is the result list of :meth:`markup`, and ``items`` is the iterable from which more arguments are read. If there is no single argument, nothing is yielded. """ origin = markup[:1] # the \markup or \markuplist command for mkup in self.read_markup_arguments(itertools.chain(markup[1:], items)): yield self.factory(lily.Markup, origin, (), mkup) break
[docs] def read_markup_arguments(self, items): """Read from items and yield nodes that can occur in markup.""" items = iter(items) for i in items: if i.is_token: if i.action is a.Text: yield self.factory(lily.MarkupWord, (i,)) elif i.action in a.Name.Function: nargs = self.get_markup_argument_count(i.text[1:]) args = [] if nargs: for arg in self.read_markup_arguments(items): args.append(arg) if not isinstance(arg, base.Comment): nargs -= 1 if nargs == 0: break yield self.factory(lily.MarkupCommand, (i,), (), *args) elif isinstance(i.obj, element.Element): yield i.obj
[docs] def get_markup_argument_count(self, command): r"""Return the number of arguments of the markup command (without ``\``). Re-implement this method if you want to add your own markup commands. The default implementation consults :meth:`LilyPond.get_markup_argument_count`. """ return LilyPond.get_markup_argument_count(command)
[docs] def create_figure(self, items): """Yield nodes to be added in a Figure.""" step = None items = iter(items) for i in items: if i.is_token: if i.action is a.Text.Music.Pitch.Figure: if step: yield step cls = lily.FigureSkip if i == '_' else lily.FigureStep step = self.factory(cls, (i,)) elif i.action is a.Text.Music.Pitch.Accidental: if step: step.append(self.factory(lily.FigureAccidental, (i,))) elif i.action is a.Literal.Character.Alteration: if step: step.append(self.factory(lily.FigureAlteration, (i,))) elif i.name == "markup": if step: yield step for step in self.create_markup(i.obj, items): break else: step = None elif isinstance(i.obj, element.Element): if step: yield step step = i.obj if step: yield step
[docs] def create_music(self, items): """Read music from items and yield Element nodes.""" return element.build_tree(MusicBuilder(self, items), ignore_type=base.Comment)
[docs] def postprocess_lyriclist(self, nodes): """Yields nodes, combining syllabe nodes with a Duration. A String, Symbol, Scheme or Markup expression is wrapped in a LyricItem node. If a lyric node is followed by an Unpitched, the unpitched's duration is added to the node, the unpitched is then discarded. Following Unpitcheds are converted to empty LyricItem nodes. """ p = None for n in nodes: if isinstance(n, lily.Unpitched): p = p or lily.LyricItem() p.extend(n) yield p else: if p: yield p if isinstance(n, (lily.String, lily.Symbol, lily.Scheme, lily.Markup)): p = lily.LyricItem(n) continue yield n p = None if p: yield p
# dispatchers for common types _pitch = Dispatcher() _action = Dispatcher() _keyword = Dispatcher() @Dispatcher def _id(self, action, token): """Dispatches for identifiers. By default, return a Symbol.""" return self.factory(lily.Symbol, (token,))
[docs] @_id(a.Number) @_action(a.Number) def number_action(self, token): r"""Called for ``Number``.""" return self.factory(lily.Int, (token,))
[docs] @_id(a.Separator) def separator_action(self, token): r"""Called for ``Delimiter.Separator``.""" return self.factory(lily.Separator, (token,))
[docs] @_action(a.Number.Float) def float_action(self, token): r"""Called for ``Number.Float``.""" return self.factory(lily.Float, (token,))
[docs] @_action(a.Number.Fraction) def fraction_action(self, token): r"""Called for ``Number.Fraction``.""" return self.factory(lily.Fraction, (token,))
[docs] @_action(a.Operator.Assignment) def assignment_action(self, token): r"""Called for ``Operator.Assignment``.""" return self.factory(lily.EqualSign, (token,))
[docs] @_action(a.Keyword) def keyword_action(self, token): r"""Called for ``Keyword``.""" return self._keyword(token.text, token)
[docs] @_action(a.Name.Builtin.Unit) def name_builtin_unit_action(self, token): r"""Called for ``Name.Builtin.Unit`` (in paper or layout block).""" return self.factory(lily.Unit, (token,))
[docs] @_action(a.Name.Type) def name_type_action(self, token): r"""Called for ``Name.Type`` (repeat mode).""" return self.factory(lily.Symbol, (token,))
[docs] @_pitch(a.Text.Music.Pitch.Octave) def pitch_octave_action(self, token): r"""Called for ``Text.Music.Pitch.Octave``.""" return self.factory(lily.Octave, (token,))
[docs] @_pitch(a.Text.Music.Pitch.Octave.OctaveCheck) def pitch_octavecheck_action(self, token): r"""Called for ``Text.Music.Pitch.Octave.OctaveCheck``.""" return self.factory(lily.OctCheck, (token,))
[docs] @_pitch(a.Text.Music.Pitch.Accidental) def pitch_accidental_action(self, token): r"""Called for ``Text.Music.Pitch.Accidental``.""" return self.factory(lily.Accidental, (token,))
[docs] @_keyword(r'\accepts') def keyword_accepts(self, token): r"""Called for Keyword ``\accepts``.""" return self.factory(lily.Accepts, (token,))
[docs] @_keyword(r'\denies') def keyword_denies(self, token): r"""Called for Keyword ``\denies``.""" return self.factory(lily.Denies, (token,))
[docs] @_keyword(r'\name') def keyword_name(self, token): r"""Called for Keyword ``\name``.""" return self.factory(lily.Name, (token,))
[docs] @_keyword(r'\alias') def keyword_alias(self, token): r"""Called for Keyword ``\alias``.""" return self.factory(lily.Alias, (token,))
[docs] @_keyword(r'\consists') def keyword_consists(self, token): r"""Called for Keyword ``\consists``.""" return self.factory(lily.Consists, (token,))
[docs] @_keyword(r'\remove') def keyword_remove(self, token): r"""Called for Keyword ``\remove``.""" return self.factory(lily.Remove, (token,))
[docs] @_keyword(r'\defaultchild') def keyword_defaultchild(self, token): r"""Called for Keyword ``\defaultchild``.""" return self.factory(lily.DefaultChild, (token,))
[docs]class LilyPondAdHocTransform(base.AdHocTransform, LilyPondTransform): """LilyPondTransform that does not keep the origin tokens. This is used to create pieces (nodes) of a LilyPond document from text, and then use that pieces to compose a larger Document or to edit an existing document. It is undesirable that origin tokens then would mistakenly be used as if they originated from the document that's being edited. """ pass
[docs]class MusicBuilder: """Helper class that reads and builds music. An instance of MusicBuilder is created and used in :meth:`LilyPondTransform.create_music`. """ _token = Dispatcher() _action = Dispatcher() _keyword = Dispatcher() _context = Dispatcher() def __init__(self, transform, items): self.transform = transform self.factory = transform.factory self.items = iter(items) self._events = [] # for direction and spanner-id self._comments = [] # for comments between pitch and duration... self.reset()
[docs] def reset(self): """Initialize all state.""" self._music = None self._duration = None self._scaling = None self._modifier = None self._articulations = None self._events.clear() self._comments.clear()
def __iter__(self): """Yield all the music from the items given at construction.""" for i in self.items: if i.is_token: # dispatch on token (text or action) meth = self._token.get(i.text) if not meth: for action in i.action: meth = self._action.get(action) if meth: break else: # TEMP print("Unknown token:", i) continue result = meth(i) else: # dispatch on object name meth = self._context.get(i.name) if not meth: if isinstance(i.obj, element.Element): yield from self.pending_music() yield i.obj else: # TEMP print("Unknown item:", i) continue result = meth(i.obj) if result: yield from result # pending stuff yield from self.pending_music()
[docs] def pending_music(self): """Yield pending music.""" music = self._music if self._duration: dur = self.factory(lily.Duration, self._duration) if self._scaling: dur.append(self._scaling) if music: music.append(dur) else: music = lily.Unpitched(dur) elif music: # move comment after pitch back to toplevel self._comments[0:] = util.pop_comments(music) if music: if self._modifier: music.extend(self._comments) music.append(self._modifier) # move comments at the end of modifier(s) back to toplevel self._comments[:] = util.pop_comments(self._modifier) if self._articulations: music.extend(self._comments) music.append(self._articulations) # move comments at end of articulations back to toplevel self._comments[:] = util.pop_comments(self._articulations) yield music yield from self._comments # if there are tweaks, tags or shapes but no articulations, the # tweak, tag or shape is meant for the next note. Output it now. for e in self._events: if isinstance(e, (lily.Tweak, lily.Tag, lily.Shape)): yield e self.reset()
[docs] def add_articulation(self, art): """Add an articulation or script.""" if self._music or self._duration: if self._events: self._events[-1].append(art) art = e = self._events[0] for f in self._events[1:]: e.append(f) e = f self._events.clear() if not self._articulations: self._articulations = lily.Articulations() self._articulations.append(art) return True else: print("Unbound event:", art) # TEMP return False
[docs] def add_spanner_id(self, node): """Return True if the node could be added to a spanner id that's being built.""" e = self._events if e and isinstance(e[-1], lily.SpannerId) and len(e[-1]) == 0: e[-1].append(node) return True return False
[docs] def add_tweak(self, node): """Return True if the node could be added to a Tweak that's being built.""" e = self._events if e and isinstance(e[-1], lily.Tweak) and len(e[-1]) < 2: e[-1].append(node) return True return False
[docs] def add_tag(self, node): r"""Return True if the node could be added to a ``\tag`` command that's being built.""" e = self._events if e and isinstance(e[-1], lily.Tag) and len(e[-1]) < 1: e[-1].append(node) return True return False
[docs] def add_shape(self, node): r"""Return True if the node could be added to a ``\shape`` command that's being built.""" e = self._events if e and isinstance(e[-1], lily.Shape) and len(e[-1]) < 2: e[-1].append(node) return True return False
[docs] def add_event_argument(self, node): r"""Try to add the node as argument to a spanner_id, tweak, tag, or shape.""" return self.add_spanner_id(node) or \ self.add_tweak(node) or \ self.add_tag(node) or \ self.add_shape(node)
[docs] @_token(r'\skip') def skip_token(self, token): r"""Called for ``\skip``.""" yield from self.pending_music() self._music = self.factory(lily.Skip, (token,))
[docs] @_token(r'\after') def after_token(self, token): r"""Called for ``\after``.""" yield from self.pending_music() self._music = self.factory(lily.After, (token,))
[docs] @_token(r'\rest') def rest_token(self, token): r"""Called for ``\rest``.""" if isinstance(self._music, lily.Note): # make it a positioned rest, reuse old pitch token if possible note = self._music self._music = rest = lily.PitchedRest(note.head, *note) rest.copy_origin_from(note) self._modifier = self.factory(lily.RestModifier, (token,))
[docs] @_token(r'\tweak') def tweak_token(self, token): r"""Called for ``\tweak``.""" self._events.append(self.factory(lily.Tweak, (token,)))
[docs] @_token(r'\noBeam') def nobeam_token(self, token): r"""Called for ``\noBeam``.""" self.add_articulation(self.factory(lily.Modifier, (token,)))
[docs] @_token(r'\tag') def tag_token(self, token): r"""Called for ``\tag``.""" self._events.append(self.factory(lily.Tag, (token,)))
[docs] @_token(r'\shape') def shape_token(self, token): r"""Called for ``\shape``.""" self._events.append(self.factory(lily.Shape, (token,)))
[docs] @_token(r'\vshape') def vshape_token(self, token): r"""Called for ``\vshape``.""" self._events.append(self.factory(lily.VShape, (token,)))
[docs] @_action(a.Text.Music.Pitch, a.Name.Pitch) def pitch_action(self, token): r"""Called for ``Text.Music.Pitch (or Name.Pitch)``.""" if not self.add_tweak(self.factory(lily.Symbol, (token,))): yield from self.pending_music() cls = lily.Q if token == 'q' else lily.Note self._music = self.factory(cls, (token,))
_rest_mapping = element.head_mapping( lily.MultiMeasureRest, lily.Rest, lily.Space )
[docs] @_action(a.Text.Music.Rest) def rest_action(self, token): r"""Called for ``Text.Music.Rest``.""" yield from self.pending_music() cls = self._rest_mapping[token.text] self._music = self.factory(cls, (token,))
[docs] @_action(a.Text.Music.Pitch.Drum) def drum_action(self, token): r"""Called for ``Text.Music.Pitch.Drum``.""" if not self.add_tweak(self.factory(lily.Symbol, (token,))): yield from self.pending_music() self._music = self.factory(lily.Drum, (token,))
[docs] @_action(a.Number.Duration) def duration_action(self, token): r"""Called for ``Number.Duration``.""" if self._duration or self._articulations: yield from self.pending_music() self._duration = [token]
[docs] @_action(a.Delimiter.Direction) def direction_action(self, token): r"""Called for ``Delimiter.Direction``.""" self._events.append(self.factory(lily.Direction, (token,)))
[docs] @_action(a.Name.Builtin.Dynamic) def dynamic_action(self, token): r"""Called for ``Name.Builtin.Dynamic``.""" self.add_articulation(self.factory(lily.Dynamic, (token,)))
# articulations that are spanners, for articulation_action() _articulations_mapping = element.head_mapping( lily.Arpeggio, lily.Glissando, lily.LaissezVibrer, lily.Melisma, lily.RepeatTie, lily.TextSpanner, lily.TrillSpanner, )
[docs] @_action(a.Name.Script.Articulation) def articulation_action(self, token): r"""Called for ``Name.Script.Articulation``.""" cls = self._articulations_mapping.get(token.text, lily.Articulation) self.add_articulation(self.factory(cls, (token,)))
# mapping for spanners in spanner_action() _spanner_mapping = { a.Name.Symbol.Spanner.Slur: lily.Slur, a.Name.Symbol.Spanner.Slur.Phrasing: lily.PhrasingSlur, a.Name.Symbol.Spanner.Tie: lily.Tie, a.Name.Symbol.Spanner.Beam: lily.Beam, a.Name.Symbol.Spanner.Ligature: lily.Ligature, a.Name.Symbol.Spanner.PesOrFlexa: lily.PesOrFlexa, }
[docs] @_action(a.Name.Symbol.Spanner) def spanner_action(self, token): r"""Called for ``Name.Symbol.Spanner.\*``.""" if token.action is a.Name.Symbol.Spanner.Id: self._events.append(self.factory(lily.SpannerId, (token,))) else: self.add_articulation(self.factory(self._spanner_mapping[token.action], (token,)))
[docs] @_action(a.Name.Type) def name_type_action(self, token): r"""Called for ``Name.Type``, e.g. a key signature mode.""" yield from self.pending_music() yield self.factory(lily.Mode, (token,))
[docs] @_action(a.Delimiter.Tremolo) def tremolo_action(self, token): r"""Called for ``Delimiter.Tremolo``.""" tremolo = self.factory(lily.Tremolo, (token,)) if token.group == 0: # next item is the duration tremolo.append(self.factory(lily.Duration, (next(self.items),))) self.add_articulation(tremolo)
[docs] @_action(a.Delimiter.Separator.PipeSymbol) def pipe_symbol_action(self, token): r"""Called for ``Delimiter.Separator.PipeSymbol``.""" yield from self.pending_music() yield self.factory(lily.PipeSymbol, (token,))
[docs] @_action(a.Delimiter.Separator.VoiceSeparator) def voice_separator_action(self, token): r"""Called for ``Delimiter.Separator.VoiceSeparator``.""" yield from self.pending_music() yield self.factory(lily.VoiceSeparator, (token,))
_chord_modifier_mapping = element.head_mapping( lily.AddSteps, lily.RemoveSteps, lily.Inversion, lily.AddInversion)
[docs] @_action(a.Delimiter.Separator.Chord) def chord_modifier_action(self, token): r"""Called for ``Delimiter.Separator.Chord``, chordmode.""" elem = self.factory(self._chord_modifier_mapping[token.text], (token,)) if token.group == 0: # next item is the pitch of an inversion note = self.factory(lily.Pitch, (next(self.items),)) elem.append(note) if self._music and not self._articulations: if not self._modifier: self._modifier = lily.ChordModifiers() self._modifier.append(elem)
[docs] @_action(a.Number) def number_action(self, token): r"""Called for ``Number``.""" elem = self.factory(lily.Int, (token,)) if not self.add_spanner_id(elem) and not self.add_tweak(elem): pass # there was no spanner id, something else?
[docs] @_action(a.Number.Float) def float_action(self, token): r"""Called for ``Number.Float``.""" elem = self.factory(lily.Float, (token,)) if not self.add_tweak(elem): yield from self.pending_music() yield elem
[docs] @_action(a.Number.Fraction) def fraction_action(self, token): r"""Called for ``Number.Fraction``.""" yield from self.pending_music() yield self.factory(lily.Fraction, (token,))
[docs] @_action(a.Name.Symbol) def symbol_action(self, token): r"""Called for ``Name.Symbol.*``.""" elem = self.factory(lily.Symbol, (token,)) if not self.add_spanner_id(elem) and not self.add_tweak(elem): pass # there was no spanner id, something else?
[docs] @_action(a.Operator.Assignment) def assignment_action(self, token): r"""Called for ``Operator.Assignment``.""" if not self._events: # '=' has no meaning inside music, but let it through at toplevel yield from self.pending_music() yield self.factory(lily.EqualSign, (token,))
[docs] @_action(a.Text.Lyric.LyricText) def lyric_text_action(self, token): r"""Called for ``Text.Lyric.LyricText.*``.""" yield from self.pending_music() origin = [token] if token.group is not None: # next tokens belong to this same word! (e.g. with _ or ~) for token in self.items: origin.append(token) if token.group < 0: # last token has negative index break self._music = self.factory(lily.LyricText, origin)
[docs] @_action(a.Delimiter.Lyric.LyricExtender) def lyric_extender_action(self, token): r"""Called for ``Delimiter.Lyric.LyricExtender``.""" yield from self.pending_music() self._music = self.factory(lily.LyricExtender, (token,))
[docs] @_action(a.Delimiter.Lyric.LyricHyphen) def lyric_hyphen_action(self, token): r"""Called for ``Delimiter.Lyric.LyricHyphen``.""" yield from self.pending_music() self._music = self.factory(lily.LyricHyphen, (token,))
[docs] @_action(a.Delimiter.Lyric.LyricSkip) def lyric_skip_action(self, token): r"""Called for ``Delimiter.Lyric.LyricSkip``.""" yield from self.pending_music() self._music = self.factory(lily.LyricSkip, (token,))
_builtin_mapping = element.head_mapping( lily.Absolute, lily.Acciaccatura, lily.AddQuote, lily.AfterGrace, lily.Alternative, lily.AppendToTag, lily.Appoggiatura, lily.Bar, lily.Break, lily.Breathe, lily.Clef, lily.Default, lily.Fixed, lily.Grace, lily.GrobDirection, lily.GrobStyle, lily.InStaffSegno, lily.KeepWithTag, lily.Key, lily.Label, lily.Mark, lily.OctaveCheck, lily.Ottava, lily.PageBreak, lily.PageTurn, lily.PartCombine, lily.Partial, lily.PushToTag, lily.QuoteDuring, lily.Relative, lily.RemoveWithTag, lily.Repeat, lily.ScaleDurations, lily.Shape, lily.ShiftDurations, lily.SlashedGrace, lily.StringTuning, lily.TagGroup, lily.Tempo, lily.Time, lily.Times, lily.Toggle, lily.Transpose, lily.Transposition, lily.Tuplet, lily.Unfolded, lily.UnfoldRepeats, lily.Unit, lily.VoiceN, lily.Volta, lily.VShape, )
[docs] @_action(a.Name.Builtin) def name_builtin_action(self, token): """Called for any Name.Builtin token.""" yield from self.pending_music() cls = self._builtin_mapping.get(token.text, lily.MusicFunction) yield self.factory(cls, (token,))
_keyword_mapping = element.head_mapping( lily.Accepts, lily.Alias, lily.Change, lily.Consists, lily.Context, lily.DefaultChild, lily.Denies, lily.Etc, lily.Hide, lily.Include, lily.Language, lily.Name, lily.New, lily.NoteMode, lily.Omit, lily.Once, lily.Override, lily.Remove, lily.Revert, lily.Sequential, lily.Set, lily.Simultaneous, lily.Temporary, lily.Undo, lily.Unset, lily.Version, )
[docs] @_action(a.Keyword) def keyword_action(self, token): """Called for any Keyword token.""" yield from self.pending_music() try: cls = self._keyword_mapping[token.text] except KeyError: yield from self._keyword(token.text, token) or () else: yield self.factory(cls, (token,))
[docs] @_keyword(r'\lyricmode', r'\lyrics', r'\lyricsto') def keyword_lyricmode(self, token): r"""Called for Keyword ``\lyricmode``, ``\lyrics`` and ``\lyricsto``.""" yield self.factory(lily.LyricMode, (token,))
[docs] @_keyword(r'\addlyrics') def keyword_addlyrics(self, token): r"""Called for Keyword ``\addlyrics``.""" yield self.factory(lily.LyricMode, (token,))
[docs] @_keyword(r'\chordmode', r'\chords') def keyword_chordmode(self, token): r"""Called for Keyword ``\chordmode`` and ``\chords``.""" yield self.factory(lily.ChordMode, (token,))
[docs] @_keyword(r'\figuremode', r'\figures') def keyword_figure(self, token): r"""Called for Keyword ``\figuremode`` and ``\figures``.""" yield self.factory(lily.FigureMode, (token,))
[docs] @_keyword(r'\drummode', r'\drums') def keyword_drummode(self, token): r"""Called for Keyword ``\drummode`` and ``\drums``.""" yield self.factory(lily.DrumMode, (token,))
[docs] @_context("chord_modifier") def chord_modifier(self, obj): """Called with the result of the ``chord_modifier`` context.""" self._modifier[-1].extend(obj)
[docs] @_context("repeat", "lyricsto", "lyricmode", "drummode", "notemode", "chordmode", "figuremode") def flatten_elements(self, obj): """Called for context that yield lists of Elements; flatten them.""" yield from obj
[docs] @_context("pitch") def pitch(self, obj): """Called for ``pitch`` context: octave, accidental, octavecheck and/or comment.""" if self._music: self._music.extend(obj) else: # only happens in erronous LilyPond input, but at least we keep the object yield from self.pending_music() yield from obj
[docs] @_context("duration") def duration(self, obj): """Called for ``duration`` context: dots, scaling.""" dots, self._scaling = obj self._duration.extend(dots)
[docs] @_context("chord", "figure") def music_element(self, obj): """An element node that is a music item (chord, figure).""" yield from self.pending_music() self._music = obj
[docs] @_context("script") def script(self, obj): """Called for ``script`` context: an articulation.""" self.add_articulation(obj)
[docs] @_context("string", "scheme", "list") def string_scheme(self, obj): """Called for ``string``, ``scheme`` or ``list`` context.""" if self._events: # after a direction: an articulation if not self.add_event_argument(obj): self.add_articulation(obj) else: # toplevel expression yield from self.pending_music() yield obj
[docs] @_context("markup") def markup(self, obj): """Called for ``markup`` context: read arguments from items.""" for node in self.transform.create_markup(obj, self.items): if self._events: # after a direction: add to the note self.add_articulation(node) else: # toplevel markup item (in lyricmode possible) yield from self.pending_music() yield node
[docs] @_context("identifier_ref") def identifier_ref(self, obj): """Called for ``identifier_ref`` context: maybe articulation.""" if self._events: # after a direction: add as articulation self.add_articulation(obj) else: # toplevel expression yield from self.pending_music() yield obj
[docs] @_context("singleline_comment", "multiline_comment") def comment(self, obj): """Called for ``singleline_comment`` and ``multiline_comment`` context. Comments are preserved as good as possible. """ if self._modifier: self._modifier.append(obj) elif self._events: self._events[-1].append(obj) elif self._articulations: self._articulations.append(obj) elif not self._music and not self._duration: # no pending music yield obj else: self._comments.append(obj) # will be added after the duration