# -*- 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/>.
"""
Elements needed for LilyPond expressions.
"""
import collections
import fractions
import itertools
import re
from .. import duration, pitch
from . import base, element, scm
[docs]class Music(element.Element):
"""Base class for all elements that contain music."""
[docs] def is_sequential(self):
"""Return True if child music nodes should be played sequentially.
This is used to compute the time position of a child: if this returns
False, every child starts at the same position; if True, preceding
nodes must be traversed in order to get the time position.
"""
return False
[docs] def properties(self):
"""Can return a :class:`~.datatypes.Properties` object with values to
keep in the context of this node.
By default, None is returned.
Inside the :meth:`Music.time_length` method you can access and modify
the accumulated properties of the current Music node via the
:attr:`~.time.TimeContext.properties` attribute of a
:class:`~.time.TimeContext` object
"""
[docs] def time_length(self, context, end=None):
"""Return the length of this expression, using a
:class:`~.time.TimeContext` handler.
If ``end`` is given it is the index to stop just before.
"""
if self.is_sequential():
return sum(context.length(n) for n in self[:end])
elif end is None:
return max((context.length(n) for n in self), default=0)
return 0
[docs]class HandleDuration(element.Element):
"""Mixin class to manipulate a Duration child via attributes.
The :attr:`duration`, :attr:`scaling` and :attr:`duration_scaling`
properties make it easy to manipulate the respective child and grandchild
nodes. Floating point values are automatically converted to Fractions, with
a limit on the denominator, so a lazy ``note.scaling = 1/3`` works
properly.
"""
duration_required = False #: Whether the Duration child is required (e.g. \skip)
duration_sets_previous = False #: Whether this Duration is stored as the previous duration for Durables without Duration
@property
def duration(self):
"""Read or set the duration.
The duration is the head value of the Duration child. The value is None
if there is no Duration child. Delete or set to None to remove the
child. (In that case the scaling also disappears.)
"""
for n in self / Duration:
return n.head
@duration.setter
def duration(self, value):
for n in self / Duration:
if value is None:
self.remove(n)
elif not isinstance(value, fractions.Fraction):
n.head = fractions.Fraction(value).limit_denominator()
else:
n.head = value
return
if value is not None:
self.add(Duration(value))
@duration.deleter
def duration(self):
for n in self / Duration:
self.remove(n)
break
@property
def scaling(self):
"""Read or set the scaling.
The scaling is the value of the DurationScaling child of the Duration
child. The value is None if there is no Duration child and 1 if there
is no DurationScaling grand child. Delete or set to None to remove the
DurationScaling grand child.
Setting the property while there is no duration child raises a
ValueError. Setting it to None or 1 removes the DurationScaling grand
child.
"""
for d in self / Duration:
scaling = 1
for s in d / DurationScaling:
scaling *= s.head
return scaling
return None
@scaling.setter
def scaling(self, value):
for d in self / Duration:
scaling = d / DurationScaling
for s in scaling:
if value in (1, None):
d.remove(s)
elif not isinstance(value, fractions.Fraction):
s.head = fractions.Fraction(value).limit_denominator()
else:
s.head = value
for s in scaling:
d.remove(s)
return
if value not in (1, None):
d.insert(0, DurationScaling(value))
return
if value not in (1, None):
raise ValueError("can't set scaling if no duration is set")
@scaling.deleter
def scaling(self):
self.scaling = None
@property
def duration_scaling(self):
"""Access duration and scaling in one go.
This value is either a two-tuple (duration, scaling) or None.
"""
for d in self / Duration:
return d.duration()
@duration_scaling.setter
def duration_scaling(self, value):
if value:
self.duration, self.scaling = value
else:
del self.duration
@duration_scaling.deleter
def duration_scaling(self):
del self.duration
[docs]class Durable(HandleDuration, Music):
"""Base class for a single musical object that takes time and can have a
Duration child.
Inherited by: :class:`Note`, :class:`Rest`, :class:`Chord`, :class:`Space`,
:class:`Skip`, :class:`LyricText` etc.
"""
duration_required = False #: Whether the Duration child is required (e.g. \skip)
duration_sets_previous = True #: Whether this Duration is stored as the previous duration for Durables without Duration
[docs] def time_length(self, context, end=None):
"""Return the length of this Durable, using a
:class:`~.time.TimeContext` handler.
For Durable, ``end`` is ignored.
"""
return context.durable_length(self)
[docs]class Pitchable(element.TextElement, Music):
"""Base class for a note or pitched rest.
The head value is the pitch name. To read, write and understand the pitch
name, use a :class:`~.pitch.PitchProcessor`.
This class provides convenient properties to manipulate the
:class:`Octave`, :class:`Accidental` and/or :class:`OctCheck` child nodes.
Inherited by: :class:`Note`, :class:`Pitch` (a pitch that is no durable
music), :class:`PitchedRest`.
"""
@property
def octave(self):
"""Read or set the octave.
The octave is an integer value, indicating how many ``'``-s or ``,``-s
are displayed after the pitch name. Automatically creates an
:class:`Octave` child if needed. Delete this attribute or set it to 0
to remove the octave.
"""
for n in self / Octave:
return n.head
return 0
@octave.setter
def octave(self, num):
for n in self / Octave:
n.head = num
return
if num != 0:
self.add(Octave(num))
@octave.deleter
def octave(self):
self[:] = self ^ Octave
@property
def accidental(self):
"""Read or set the accidental.
The accidental is ``None``, ``"cautionary"`` or ``"forced"``.
Automatically creates an :class:`Accidental` child if needed.
Delete this attribute or set it to None to remove the accidental.
"""
for n in self / Accidental:
return n.head
@accidental.setter
def accidental(self, value):
for n in self / Accidental:
if not value:
self.remove(n)
else:
n.head = value
return
if value:
self.add(Accidental(value))
@accidental.deleter
def accidental(self):
self[:] = self ^ Accidental
@property
def oct_check(self):
"""Read or set the octave check.
The octave check is an integer value, or None, when no octave check is
there. Automatically creates an :class:`OctCheck` child if set. Delete
this attribute or set it tot ``None`` to remove the octave check.
"""
for n in self / OctCheck:
return n.head
@oct_check.setter
def oct_check(self, num):
for n in self / OctCheck:
if num is None:
self.remove(n)
else:
n.head = num
return
if num is not None:
self.add(OctCheck(num))
@oct_check.deleter
def oct_check(self):
self[:] = self ^ OctCheck
[docs]class Reference(element.Element):
r"""Base class for an Element that (potentially) refers to another node,
possibly in another DOM tree.
This is used te get the value of a variable, e.g. for the IdentifierRef and
the MarkupCommand element types. The :meth:`get_value` method returns the
value. Here is an example of how it works::
>>> from quickly.dom import read
>>> m = read.lily_document(r'''
... titled = "blurk"
...
... \header {
... title = "Wilbert"
... composer = \title
... }
... ''', True)
>>> m.dump()
<lily.Document (2 children)>
├╴<lily.Assignment titled (3 children)>
│ ├╴<lily.Identifier (1 child)>
│ │ ╰╴<lily.Symbol 'titled' [1:7]>
│ ├╴<lily.EqualSign [8:9]>
│ ╰╴<lily.String 'blurk' [10:17]>
╰╴<lily.Header (2 children) [19:70]>
├╴<lily.Assignment title (3 children)>
│ ├╴<lily.Identifier (1 child)>
│ │ ╰╴<lily.Symbol 'title' [31:36]>
│ ├╴<lily.EqualSign [37:38]>
│ ╰╴<lily.String 'Wilbert' [39:48]>
╰╴<lily.Assignment composer (3 children)>
├╴<lily.Identifier (1 child)>
│ ╰╴<lily.Symbol 'composer' [51:59]>
├╴<lily.EqualSign [60:61]>
╰╴<lily.IdentifierRef 'title' [62:68]>
>>> n = m[1].composer
>>> n
<lily.IdentifierRef 'title' [62:68]>
>>> n.get_value()
<lily.String 'Wilbert' [39:48]>
>>> n.head = "titled"
>>> n.get_value()
<lily.String 'blurk' [10:17]>
The ``composer`` field in the header is set to the value of the title
field. When requesting the value, the value of the title assignment is
returned. When we change the name of the variable reference to ``titled``,
it finds the "blurk" value in the toplevel document.
A reference can also point to another file. A :class:`~.scope.Scope` is used
to define the context of a file and the desired path to look for ``\include``
files. Here is an example. First we create two LilyPond files::
>>> with open('file_a.ly', 'w') as f:
... f.write("music = { c d e f g }\n")
...
22
>>> with open('file_b.ly', 'w') as f:
... f.write('\\include "file_a.ly"\n\n{ \\music }\n')
...
33
Then we load ``file_b.ly`` into a parce document, and we try to find the
value of the ``\music`` variable::
>>> import quickly
>>> d = quickly.load('file_b.ly')
>>> print(d.text())
\include "file_a.ly"
{ \music }
>>> m = d.get_transform(True)
>>> m.dump()
<lily.Document (2 children)>
├╴<lily.Include 'file_a.ly' (1 child) [0:8]>
│ ╰╴<lily.String 'file_a.ly' [9:20]>
╰╴<lily.MusicList (1 child) [22:32]>
╰╴<lily.IdentifierRef 'music' [24:30]>
>>> m[1][0]
<lily.IdentifierRef 'music' [24:30]>
>>> print(m[1][0].get_value())
None
We see that the returned value is None, meaning that the definition of
``music`` was not found in ``file_b.ly``. Now, we create a
:class:`~.scope.Scope` to find included files, and try it again::
>>> from quickly.dom.scope import Scope
>>> s = Scope(d)
>>> m[1][0].get_value(s)
<lily.MusicList (5 children) [8:21]>
>>> m[1][0].get_value(s).dump()
<lily.MusicList (5 children) [8:21]>
├╴<lily.Note 'c' [10:11]>
├╴<lily.Note 'd' [12:13]>
├╴<lily.Note 'e' [14:15]>
├╴<lily.Note 'f' [16:17]>
╰╴<lily.Note 'g' [18:19]>
>>> m[1][0].get_value_with_scope(s)
(<lily.MusicList (5 children) [8:21]>, <Scope 'file_a.ly'>)
The ``music`` in ``file_a.ly`` is found. The :meth:`get_value_with_scope`
method also returns the scope the definition was found in, which can be
used to recursively resolve Reference nodes in the returned node.
"""
[docs] def get_value(self, scope=None, wait=True):
"""Find the value this variable refers to.
Returns the value if found, and None otherwise. For the arguments, see
:meth:`get_value_with_scope`.
"""
result = self.get_value_with_scope(scope)
if result:
return result[0]
[docs] def get_value_with_scope(self, scope=None, wait=True):
"""Find the value this variable refers to.
Searches for Assignments. If found, returns a two-tuple (value, scope).
Otherwise None. The scope is the scope the value was found in.
The ``scope``, if given, is used to resolve include files. If no scope
is given, only searches the current DOM document; the returned scope is
then always None.
If a scope is given, include commands are followed and ``wait``
determines whether to wait for ongoing transformations of external DOM
documents. If wait is False, and a transformation is not yet finished,
a value found in an included document will not be returned.
"""
return Lookup(self, scope, wait).find_assignment(self.get_name())
[docs] def get_name(self):
"""Implement to return the name the ``get_value()`` methods search for."""
raise NotImplementedError
class _Variable:
"""A property that makes setting an Assignment easier.
This can only be used for simple variable names. An underscore in the name
is converted to a hyphen. Use this in classes that mix in
lily.HandleAssignments::
class Header(HandleAssignments):
title = _Variable("The title of the document.")
When setting a variable, only ``int`` and ``str`` are allowed, or an
Element node of course. When getting a value, for an Int or String node the
head value is returned, for other element types the node itself.
"""
def __init__(self, docstring=None):
self.__doc__ = docstring
def __set_name__(self, owner, name):
self.name = name.replace('_', '-')
def __get__(self, instance, owner=None):
if instance:
return instance.get_variable(self.name)
return self
def __set__(self, instance, value):
if value is None:
instance.unset_variable(self.name)
else:
instance.set_variable(self.name, value)
def __delete__(self, instance):
instance.unset_variable(self.name)
class _ConvertUnpitchedToDuration(element.Element):
"""Mixin class to convert Unpitched arguments to their Duration."""
def add_argument(self, node):
"""Reimplemented to pick the Duration of an Unpitched node."""
if isinstance(node, Unpitched):
for node in node:
node = node.copy_with_origin()
break
super().add_argument(node)
class _ConvertUnpitchedToInt(element.Element):
"""Mixin class to convert Unpitched arguments to an Int."""
def add_argument(self, node):
"""Reimplemented to read the Duration of an Unpitched node as a Int."""
if isinstance(node, Unpitched):
for dur in node / Duration:
node = convert_duration_to_int(dur) or node
break
super().add_argument(node)
[docs]class HandleAssignments(element.Element):
"""Mixin class to handle Assignment children in a convenient way."""
[docs] def find_assignment(self, name):
"""Find the Assignment with the specified name."""
for n in self/Assignment:
for v in n/Identifier:
if v.get_name() == name:
return n
[docs] def get_variable(self, name):
"""Convenience method to find the value of the named variable.
Finds an Assignment child that assigns a value to a Identifier with the
specified ``name``. Returns the Element node representing the value,
or None if no assignment with that name exists.
When the node is a String or an Int, its head value is returned.
If no assignment with the name can be found, None is returned.
For the ``name``, see :meth:`Identifier.set_name`.
"""
assignment = self.find_assignment(name)
if assignment:
node = assignment.get_value()
value = create_value_from_element(node)
if value is not None:
return value
return node
[docs] def set_variable(self, name, value):
"""Convenience method to add or replace a variable assignment.
If an Assignment exists with the named variable, replaces its node
value; otherwise appends a new Assignment.
If the value is an Element node, it is used directly. If it is an
integer, an Int element is created; if it is a string, a String element
is created. (If such element was already in use, only the head value
is changed.)
"""
node = create_element_from_value(value)
assignment = self.find_assignment(name)
if assignment:
assignment[-1] = node
else:
self.append(Assignment.with_name(name, node))
[docs] def unset_variable(self, name):
"""Convenience method to delete a variable assignment.
If an Assignment exists with the named variable, it is removed from its
parent.
"""
assignment = self.find_assignment(name)
if assignment:
self.remove(assignment)
[docs] def variables(self):
"""Convenience method to return a list of the available variable names."""
return list(v.get_name()
for n in self/Assignment
for v in n/Identifier)
[docs]class Document(HandleAssignments, base.Document):
"""A full LilyPond source document."""
@property
def version(self):
"""The LilyPond version number, as a tuple of ints (may be empty)."""
for v in self//Version:
return v.version
return ()
@version.setter
def version(self, version):
for v in self//Version:
v.version = version
return
self.insert(0, Version(version=version))
[docs]class Number(element.TextElement):
"""Base class for numeric values."""
[docs] def write_head(self):
return str(self.head)
[docs] @classmethod
def read_head(cls, origin):
raise NotImplementedError
[docs]class Int(Number):
"""An integer number."""
[docs] @classmethod
def check_head(cls, head):
return type(head) is int
[docs] @classmethod
def read_head(cls, origin):
return int(origin[0].text)
[docs] def signatures(self):
yield Unit,
[docs]class Fraction(Number):
"""A fraction, like ``1/2``.
The head value is a two-tuple of ints (numerator, denominator).
"""
[docs] def fraction(self):
"""Return the head value as a :class:`fractions.Fraction`."""
return fractions.Fraction(*self.head)
[docs] @classmethod
def check_head(cls, head):
return isinstance(head, tuple) and len(head) == 2 and \
all(isinstance(v, int) for v in head)
[docs] def repr_head(self):
return self.write_head()
[docs] @classmethod
def read_head(cls, origin):
return tuple(map(int, origin[0].text.split('/')))
[docs] def write_head(self):
return '/'.join(map(str, self.head))
[docs]class Float(Number):
"""A floating point number."""
[docs] @classmethod
def read_head(cls, origin):
return float(origin[0].text)
[docs] @classmethod
def check_head(cls, head):
return isinstance(head, (int, float))
[docs] def signatures(self):
yield Unit,
[docs]class Symbol(element.TextElement):
"""A symbol (unquoted text piece)."""
[docs]class String(base.String):
r"""A quoted string."""
[docs]class Scheme(element.TextElement):
"""A Scheme expression in LilyPond.
A Scheme expression can start with ``$``, ``#``, ``$@`` or ``#@``.
The latter two are rarely used; they unroll a list in the surrounding
expression.
A Scheme expression starting with a dollar sign is directly executed by
LilyPond when encountered in a source file, it is then ignored when it is
no valid expression; an expression starting with a hash sign is evaluated
later, when evalating the music expression.
"""
space_before = space_after = " "
[docs] @classmethod
def check_head(cls, head):
return head in ('$', '#', '$@', '#@')
[docs]class Spanner(element.MappingElement):
r"""Base class for spanner elements, that start or stop.
Specify ``"start"`` or ``"stop"`` to the constructor, and put the texts
that are displayed for either in the ``spanner_start`` and ``spanner_stop``
attribute.
"""
spanner_start = "<start>"
spanner_stop = "<stop>"
def __init_subclass__(cls, **kwargs):
cls.mapping = {cls.spanner_start: "start", cls.spanner_stop: "stop"}
super().__init_subclass__(**kwargs)
[docs] def find_parallel(self, limit=0):
r"""Try to find the other end of this spanner. May return None.
Does not look outside of the current New or Assignment node. The
``limit`` can be used to further restrict the number of nodes searched,
e.g. to prevent slowness in text editors that do not need to highlight
items far offscreen.
For example::
>>> from quickly.dom import lily, read
>>> n = read.lily(r"{ c\=1( d e f g\=2) a\=1) }", True)
>>> n.dump()
<lily.MusicList (6 children) [0:27]>
├╴<lily.Note 'c' (1 child) [2:3]>
│ ╰╴<lily.Articulations (1 child)>
│ ╰╴<lily.SpannerId (2 children) [3:5]>
│ ├╴<lily.Int 1 [5:6]>
│ ╰╴<lily.Slur 'start' [6:7]>
├╴<lily.Note 'd' [8:9]>
├╴<lily.Note 'e' [10:11]>
├╴<lily.Note 'f' [12:13]>
├╴<lily.Note 'g' (1 child) [14:15]>
│ ╰╴<lily.Articulations (1 child)>
│ ╰╴<lily.SpannerId (2 children) [15:17]>
│ ├╴<lily.Int 2 [17:18]>
│ ╰╴<lily.Slur 'stop' [18:19]>
╰╴<lily.Note 'a' (1 child) [20:21]>
╰╴<lily.Articulations (1 child)>
╰╴<lily.SpannerId (2 children) [21:23]>
├╴<lily.Int 1 [23:24]>
╰╴<lily.Slur 'stop' [24:25]>
>>> slur = n.find_descendant(6)
>>> slur
<lily.Slur 'start' [6:7]>
>>> slur.find_parallel()
<lily.Slur 'stop' [24:25]>
"""
upto = next(self << (Assignment, New, Document, Score), None)
nodes = self.forward(upto) if self.head == "start" else self.backward(upto)
if limit:
nodes = itertools.islice(nodes, limit)
cls, head = type(self), self.head
parallel = lambda n: type(n) is cls and n.head != head
spanner_id = self.left_sibling() if isinstance(self.parent, SpannerId) else None
if spanner_id:
spanner_ok = lambda n: isinstance(n.parent, SpannerId) and spanner_id.equals(n.left_sibling())
else:
spanner_ok = lambda n: not isinstance(n.parent, SpannerId)
for n in filter(lambda n: parallel(n) and spanner_ok(n), nodes):
return n
[docs]class Block(HandleAssignments, element.BlockElement):
"""Base class for a block, e.g. score, paper, etc.
Newlines are placed by default between all child nodes. There are
convenience methods to access variables inside a block.
"""
space_before = space_after = space_after_head = space_before_tail = space_between = '\n'
head = '<fill in> {'
tail = '}'
[docs]class Book(Block):
r"""A \book { } block."""
head = r"\book {"
[docs]class BookPart(Block):
r"""A \bookpart { } block."""
head = r"\bookpart {"
[docs]class Score(Block):
r"""A \score { } block."""
head = r"\score {"
[docs]class Paper(Block):
r"""A \paper { } block.
The most used paper variables can be set using properties, which
auto-convert string, int and boolean values. Where LilyPond uses hyphens in
paper variables, these properties use underscores.
"""
head = r"\paper {"
paper_height = _Variable("Paper height.")
top_margin = _Variable("Top margin.")
bottom_margin = _Variable("Bottom margin.")
ragged_bottom = _Variable("Whether to have a ragged bottom (bool).")
ragged_last_bottom = _Variable("Whether to have a ragged bottom on the last page (bool).")
markup_system_spacing = _Variable("Spacing between markup and first system.")
score_markup_spacing = _Variable("Spacing between score and markup.")
score_system_spacing = _Variable("Spacing between two adjacent scores.")
system_system_spacing = _Variable("Spacing between systems of one score.")
markup_markup_spacing = _Variable("Spacing between two markups.")
last_bottom_spacing = _Variable("Spacing between the last system or markup and the page bottom.")
top_system_spacing = _Variable("Spacing between page top and first system.")
top_markup_spacing = _Variable("Spacing between page top and first markup.")
paper_width = _Variable("Paper width.")
line_width = _Variable("Line witdh.")
left_margin = _Variable("Left margin.")
right_margin = _Variable("Right margin.")
check_consistency = _Variable("Check whether all width settings fit.")
ragged_right = _Variable("Whether to fill out the systems to the right (bool).")
ragged_last = _Variable("Whether to fill out the last system to the right (bool).")
two_sided = _Variable("Whether to have mirrored margins for left and right pages.")
inner_margin = _Variable("Margin at binding side.")
outer_margin = _Variable("Margin at outer side.")
binding_offset = _Variable("Extra offset for inner-margin.")
horizontal_shift = _Variable("Amount al systems and markups are shifted to the right.")
indent = _Variable("Indent distance for the first system.")
short_indent = _Variable("Indent distance for all other systems.")
max_systems_per_page = _Variable("The maximum number of systems on a page.")
min_systems_per_page = _Variable("The minimum number of systems on a page.")
systems_per_page = _Variable("How many systems to put on a page.")
system_count = _Variable("The number of systems to create.")
page_breaking = _Variable("Page-breaking algorithm to use.")
page_breaking_system_system_spacing = _Variable("Specially adjust spacing for page breaker.")
page_count = _Variable("The number of pages to be used.")
blank_page_penalty = _Variable("Penalty for having a blank page.")
blank_last_page_penalty = _Variable("Penalty for ending on a left page.")
blank_after_score_page_penalty = _Variable("Penalty for having a blank page before a score.")
auto_first_page_number = _Variable("Automatically choose whether to start with even or odd page number.")
first_page_number = _Variable("The page number for the first page.")
print_first_page_number = _Variable("Print the page number on the first page (bool).")
print_page_number = _Variable("Print page numbers anyway (bool).")
page_spacing_weight = _Variable("Relative importance of page and line spacing.")
print_all_headers = _Variable("Whether to print all headers in each score (bool).")
system_separator_markup = _Variable("Markup to use between systems.")
[docs]class Layout(Block):
r"""A \layout { } block.
The most used layout variables can be set using properties, which
auto-convert string, int and boolean values. Where LilyPond uses hyphens in
paper variables, these properties use underscores.
"""
head = r"\layout {"
line_width = _Variable("Line witdh.")
ragged_right = _Variable("Whether to fill out the systems to the right (bool).")
ragged_last = _Variable("Whether to fill out the last system to the right (bool).")
indent = _Variable("Indent distance for the first system.")
short_indent = _Variable("Indent distance for all other systems.")
system_count = _Variable("The number of systems to create.")
[docs]class Midi(Block):
r"""A \midi { } block."""
head = r"\midi {"
[docs]class With(Block):
r"""A \with { } block."""
head = r"\with {"
space_before = space_after = " "
[docs]class LayoutContext(Block):
r"""A \context { } block within \layout or \midi."""
head = r"\context {"
[docs]class EqualSign(element.HeadElement):
r"""An equal sign (``=``)."""
head = "="
space_before = space_after = " "
[docs]class Separator(element.TextElement):
"""A separator."""
[docs]class List(element.Element):
"""A list consisting of String, Scheme, Int or Symbol elements.
Separated by Separator elements; may also contain Comment nodes.
"""
[docs] def get_list(self):
"""Convenience method to get a tuples with the contents of the list.
Comment and Scheme nodes are ignored; for Symbol and String elements
Python strings are returned, and for Int elements integer values.
"""
return tuple(node.head for node in self / (Symbol, String, Int))
[docs] def set_list(self, iterable):
"""Replaces the contents of this List with nodes converted
from the iterable.
Strings are converted to Symbol if possible, else String, and integers
to Int nodes.
"""
self.clear()
nodes = filter_map(make_list_node, iterable)
for n in nodes:
self.append(n)
for n in nodes:
self.append(Separator(',' if isinstance(n, Int) else '.'))
self.append(n)
[docs]class Assignment(element.Element):
"""A variable = value construct.
The first node is a Identifier element, then an EqualSign, and then the
value.
"""
space_before = space_after = '\n'
[docs] @classmethod
def with_name(cls, name, node):
"""Convenience constructor to create a complete Assignment.
Automatically creates a Identifier child node for the ``name``, an
EqualSign node, and appends the specified ``node`` as the value of the
assignment. For the ``name``, see :meth:`Identifier.set_name`.
"""
return cls(Identifier.with_name(name), EqualSign(), node)
[docs] def repr_head(self):
"""If available, show the name of our identifier."""
for child in self / Identifier:
return child.write()
[docs] def get_name(self):
"""Return the name of our identifier as a string or tuple."""
for child in self / Identifier:
return child.get_name()
[docs] def get_value(self):
"""Return the value after the equalsign."""
for child in self / EqualSign:
return child.right_sibling()
[docs]class Identifier(List):
"""A variable name, the first node is always a Symbol or String.
Further contains Symbol, String, Separator, Int or Scheme. This
element is created when a List is encountered in an assignment by the
transformer (see
:meth:`~quickly.lang.lilypond.LilyPondTransform.handle_assignments`).
"""
[docs] @classmethod
def with_name(cls, name):
"""Create a Identifier with specified name."""
v = cls()
v.set_name(name)
return v
[docs] def get_name(self):
"""Convenience method to get the name of this variable.
This can be a plain string or a tuple. It is a tuple when the variable
name consists of multiple parts, separated by dots. The first item in
the tuple is always a string, but the other items might also be
numbers.
"""
heads = self.get_list()
if len(heads) == 1:
return heads[0]
return heads
[docs] def set_name(self, name):
"""Convenience method to set the name of this variable.
In most cases the name is an alphanumeric identifier, but it can be any
string (in that case it is automatically quoted) or a tuple of names,
strings and even numbers. The first item in the tuple always must be a
name or string. An alphanumeric string is turned into a :class:`Symbol`
element, a string containing "illegal" characters into a
:class:`String` element, and an integer value into a :class:`Int`
element.
"""
if type(name) is str:
name = name,
self.set_list(name)
[docs]class IdentifierRef(base.BackslashCommand, Reference):
r"""A ``\variable`` name.
The first symbol part is in the head of this element. Additional nodes can
be Symbol, String, Separator, Int or Scheme.
For the ``\"name"``, construct, head is the empty string, and the first
child is a String. Otherwise, if there are child nodes, the first child is
a Separator.
For the constructor, the backslash is not needed::
>>> from quickly.dom.lily import *
>>> var = IdentifierRef('music')
>>> var.write()
'\\music'
>>> var = IdentifierRef.with_name(('music', 1))
>>> var.write()
'\\music.1'
"""
[docs] @classmethod
def with_name(cls, name):
"""Convenience method to create a IdentifierRef with specified name.
This is especially useful with complicated names that are not a
simple symbol.
"""
v = cls()
v.set_name(name)
return v
[docs] def get_name(self):
"""Convenience method to get the name of this variable.
The backslash is not returned. The name can be a plain string or a
tuple. It is a tuple when the variable name consists of multiple parts,
separated by dots. The first item in the tuple is always a string, but
the other items might also be numbers.
"""
names = []
if self.head:
names.append(self.head)
for n in self / (Symbol, String, Int):
names.append(n.head)
if len(names) == 1:
return names[0]
return tuple(names)
[docs] def set_name(self, name):
"""Convenience method to set the name of this variable.
In most cases the name is an alphanumeric identifier, but it can be any
string (in that case it is automatically quoted) or a tuple of names,
strings and even numbers. The first item in the tuple always must be a
name or string. An alphanumeric string is turned into a :class:`Symbol`
element, a string containing "illegal" characters into a
:class:`String` element, and an integer value into a :class:`Int`
element.
A backslash need not to be prepended.
"""
if type(name) is str:
name = name,
nodes = filter_map(make_list_node, name)
self.clear()
for n in nodes:
if isinstance(n, Symbol):
self.head = n.head
else:
self.head = ''
self.append(n)
for n in nodes:
self.append(Separator('.'))
self.append(n)
[docs]class MusicFunction(base.BackslashCommand, Music):
r"""A generic music function with a backslash, like ``\stemUp``
To be used if there is no special Element type for the music function.
When manually constructing this element, the initial backslash need not
to be given. Example::
>>> from quickly.dom.lily import MusicFunction
>>> MusicFunction('stemUp').write()
'\\stemUp'
"""
space_between = space_after_head = " "
[docs] def signatures(self):
if self.head == "defineBarLine":
yield String, Scheme
[docs]class Context(element.HeadElement, Music):
r"""``\context ...``."""
space_between = space_after_head = " "
head = r'\context'
[docs] def signatures(self):
yield Symbol, MUSIC
yield Symbol, With, MUSIC
yield Symbol, EqualSign, (String, Symbol), MUSIC
yield Symbol, EqualSign, (String, Symbol), With, MUSIC
[docs]class New(Context):
r"""``\new ...``."""
head = r'\new'
[docs]class Change(element.HeadElement, Music):
r"""``\change ...``."""
space_between = space_after_head = " "
head = r'\change'
[docs] def signatures(self):
yield Symbol, EqualSign, (String, Symbol)
[docs]class AddQuote(element.HeadElement):
r"""An ``\addQuote`` command, at toplevel."""
space_between = space_after_head = " "
head = r'\addQuote'
[docs] def signatures(self):
yield (Identifier, String, Symbol), MUSIC
[docs]class QuoteDuring(element.HeadElement, Music):
r"""A ``\quoteDuring`` command."""
space_between = space_after_head = " "
head = r'\quoteDuring'
[docs] def signatures(self):
yield (List, String, Symbol), MUSIC
[docs]class ApplyContext(element.HeadElement):
r"""The ``\applyContext`` command."""
space_between = space_after_head = " "
head = r'\applyContext'
[docs] def signatures(self):
yield Scheme,
[docs]class ApplyMusic(element.HeadElement):
r"""The ``\applyMusic`` function."""
space_between = space_after_head = " "
head = r'\applyMusic'
[docs] def signatures(self):
yield Scheme, MUSIC
[docs]class ApplyOutput(element.HeadElement):
r"""The ``\applyOutput`` command."""
space_between = space_after_head = " "
head = r'\applyOutput'
[docs] def signatures(self):
yield SYMBOL, Scheme
[docs]class Relative(element.HeadElement, Music):
"""Relative music."""
head = r'\relative'
space_between = space_after_head = " "
[docs] def signatures(self):
yield Pitchable, MUSIC
yield MUSIC,
[docs] def add_argument(self, node):
"""Reimplemented to turn an added Note argument into a Pitch."""
if isinstance(node, Note) and sum(1 for n in self / Pitchable) < 1:
node = node.to_pitch()
return super().add_argument(node)
[docs]class Absolute(element.HeadElement, Music):
"""Absolute music."""
head = r'\absolute'
space_between = space_after_head = " "
[docs] def signatures(self):
yield MUSIC,
[docs]class Fixed(element.HeadElement, Music):
"""Fixed music."""
head = r'\fixed'
space_between = space_after_head = " "
[docs] def signatures(self):
yield Pitchable, MUSIC
[docs] def add_argument(self, node):
"""Reimplemented to turn an added Note argument into a Pitch."""
if isinstance(node, Note) and sum(1 for n in self / Pitchable) < 1:
node = node.to_pitch()
return super().add_argument(node)
[docs]class Transpose(element.HeadElement, Music):
"""Transposed music."""
head = r'\transpose'
space_between = space_after_head = " "
[docs] def signatures(self):
yield Pitchable, Pitchable, MUSIC
[docs] def add_argument(self, node):
"""Reimplemented to turn 2 added Note arguments into Pitch arguments."""
if isinstance(node, Note) and sum(1 for n in self / Pitchable) < 2:
node = node.to_pitch()
return super().add_argument(node)
[docs]class Repeat(element.HeadElement, Music):
"""Repeated music."""
head = r'\repeat'
space_between = space_after_head = " "
[docs] def signatures(self):
yield Symbol, INT, MUSIC
yield Symbol, INT, MUSIC, Alternative
[docs]class Alternative(element.HeadElement, Music):
"""Alternative music for repeats."""
head = r'\alternative'
space_between = space_after_head = " "
[docs] def signatures(self):
yield MusicList,
[docs]class UnfoldRepeats(element.HeadElement, Music):
r"""The ``\unfoldRepeats`` command."""
space_between = space_after_head = " "
head = r'\unfoldRepeats'
[docs] def signatures(self):
yield MUSIC,
[docs]class Unfolded(element.HeadElement, Music):
r"""The ``\unfolded`` command."""
head = r'\unfolded'
space_between = space_after_head = " "
[docs] def signatures(self):
yield MUSIC,
[docs]class Volta(element.HeadElement, Music):
r"""The ``\volta`` command."""
head = r'\volta'
space_between = space_after_head = " "
[docs] def signatures(self):
yield (List, Scheme), MUSIC
[docs]class Transposition(element.HeadElement, Music):
r"""A ``\tranposition`` command."""
head = r'\transposition'
space_between = space_after_head = " "
[docs] def signatures(self):
yield Pitchable,
[docs] def add_argument(self, node):
"""Reimplemented to turn an added Note argument into a Pitch."""
if isinstance(node, Note) and sum(1 for n in self / Pitchable) < 1:
node = node.to_pitch()
return super().add_argument(node)
[docs]class Ottava(_ConvertUnpitchedToInt, element.HeadElement, Music):
r"""An ``\ottava`` command."""
space_between = space_after_head = " "
head = r'\ottava'
[docs] def signatures(self):
yield NUMBER,
[docs]class MusicList(element.BlockElement, Music):
"""A list of music items between ``{`` ... ``}``."""
space_after_head = space_before_tail = space_between = " "
head = "{"
tail = "}"
[docs] def is_sequential(self):
"""Return False when the parent is a Simultaneous command, True otherwise."""
return not isinstance(self.parent, Simultaneous)
[docs] def indent_align_indices(self):
"""How to align child nodes if on the same line as an indenting char."""
yield 0
[docs]class SimultaneousMusicList(MusicList):
"""A list of music items between ``<<`` ... ``>>``."""
head = "<<"
tail = ">>"
[docs] def is_sequential(self):
"""Return always False."""
return False
[docs]class Sequential(element.HeadElement, Music):
r"""The ``\sequential`` command, has one MusicList child."""
head = r'\sequential'
space_between = space_after_head = " "
[docs] def signatures(self):
yield MusicList,
[docs]class Simultaneous(element.HeadElement, Music):
r"""The ``\simultaneous`` command, has one MusicList child."""
head = r'\simultaneous'
space_between = space_after_head = " "
[docs] def signatures(self):
yield MusicList,
[docs]class LyricMode(base.BackslashCommand, InputMode):
r"""``\lyricmode``, ``\lyrics`` or ``\lyricsto``."""
[docs] def signatures(self):
if self.head == r'lyricsto':
yield (String, Symbol), MUSIC
else:
yield MUSIC,
[docs]class ChordMode(base.BackslashCommand, InputMode):
r"""``\chordmode`` or ``\chords``."""
[docs]class DrumMode(base.BackslashCommand, InputMode):
r"""``\drummode`` or ``\drums``."""
[docs]class NoteMode(element.HeadElement, InputMode):
r"""``\notemode``."""
head = r'\notemode'
[docs]class Chord(Durable):
"""A chord. Must have a ChordBody element."""
[docs] def time_length(self, context, end=None):
"""Return the length of this Durable, using a
:class:`~.time.TimeContext` handler.
For Chord, ``end`` is ignored; returns 0 if the chord is empty,
in accordance with LilyPond's behaviour.
"""
for body in self:
if any(body / Note):
return super().time_length(context, end)
return 0
[docs] def child_order(self):
yield ChordBody, Duration, Articulations, base.Comment
[docs]class ChordBody(element.BlockElement):
"""The body of a chord ``<`` ... ``>``.
Always the child of a Chord, which can have a duration and articulations.
Contains Note elements.
"""
space_between = " "
head = "<"
tail = ">"
[docs] def indent_align_indices(self):
"""How to align child nodes if on the same line as an indenting char."""
yield 0
[docs]class Note(Pitchable, Durable):
"""A musical note."""
[docs] def child_order(self):
yield Octave, Accidental, OctCheck, Duration, Articulations, base.Comment
[docs] def to_pitch(self):
r"""Convenience function to create a :class:`Pitch` from this note.
This is used when this Note is added to a command where the note has
not a musical meaning, but just a pitch is intended, e.g. ``\key`` or
``\transpose``, etc.
"""
p = Pitch(self.head, *self)
p.copy_origin_from(self)
return p
[docs]class Pitch(Pitchable):
r"""A pitch name.
This is used as pitch argument for ``\transpose``, ``\tranposition``,
``\fixed``, ``\relative``, ``\key`` etc. The difference with :class:`Note`
is that a Pitch is not a Durable and can't have a duration or
articulations.
"""
[docs] def child_order(self):
yield Octave, Accidental, OctCheck, base.Comment
[docs]class Unpitched(Durable):
"""An unpitched note, always has a Duration child."""
duration_required = True #: always needs a duration
[docs] def child_order(self):
yield Duration, Articulations, base.Comment
[docs]class RestType(Durable):
"""Base class for Rest, PitchedRest and MultiMeasureRest."""
[docs] def child_order(self):
yield Duration, Articulations, base.Comment
[docs]class Rest(element.HeadElement, RestType):
head = "r"
"""A normal rest (``r``)."""
[docs]class MultiMeasureRest(Rest):
head = "R"
"""A multi-measure rest (``R``)."""
[docs]class PitchedRest(Pitchable, RestType):
r"""A pitched rest.
This rest has a pitchname but also a RestModifier child, e.g. ``c\rest``.
It is a normal rest, but vertically positioned using a pitch name, which is
the head value. This element can also have an Octave or OctCheck.
"""
[docs] def child_order(self):
yield Octave, Accidental, OctCheck, Duration, RestModifier, Articulations, base.Comment
[docs]class Space(element.HeadElement, Durable):
"""A space (``s``)."""
head = "s"
[docs] def child_order(self):
yield Duration, Articulations, base.Comment
[docs]class Skip(element.HeadElement, Durable):
r"""A ``\skip``.
For LilyPond version upto and including 2.22, must have a Duration child.
For LilyPond version from 2.23.6, may have a Music argument instead, which
then defines the length of the skip.
With a duration, LilyPond creates a SkipMusic event; with a music argument,
LilyPond creates a SkippedMusic construct with music that's just not printed
and does not create outputs, but whose length is computed precisely.
We handle both; the ``duration_required`` means here that it may not be
removed if it's there. If there is no direct Duration child, the Skip
is not to be regarded as a regular Durable, but rather as a music function.
"""
head = r'\skip'
space_after_head = " "
duration_required = True #: always needs a duration
duration_sets_previous = False #: the "previous" duration is not changed by \skip
[docs] def time_length(self, context, end=None):
r"""Return the length of this Durable, using a
:class:`~.time.TimeContext` handler.
Reimplemented to handle the case where ``\skip`` has a music argument
instead of a Duration argument.
"""
if any(self / Duration):
return super().time_length(context, end)
return super(Durable, self).time_length(context, end)
[docs] def signatures(self):
yield Duration,
yield MUSIC,
[docs] def child_order(self):
yield Duration, base.Comment
[docs]class After(element.HeadElement, HandleDuration, Music):
r"""An ``\after``. Must have a Duration child and an event."""
head = r'\after'
[docs]class Q(element.HeadElement, Durable):
"""A ``q``, repeating the previous chord.
The repeated chord always has the same absolute pitch, Octave childs are
not possible. LilyPond signals a warning if there is no previous chord in
the current music expression, and the q becomes a skip.
Articulations attached to the repeated chord or to its individual notes are
not copied, but internal tweaks to the noteheads are.
"""
head = 'q'
[docs] def child_order(self):
yield Duration, Articulations, base.Comment
[docs]class Drum(element.TextElement, Durable):
"""A drum note."""
[docs] def child_order(self):
yield Duration, Articulations, base.Comment
[docs]class Accidental(element.MappingElement):
"""The accidental after a note.
Can be ``cautionary`` or ``forced``.
"""
mapping = {
'?': 'cautionary',
'!': 'forced',
}
[docs]class Octave(element.TextElement):
"""The octave after a note.
The head value is the number of ``'`` (if positive) or the number of ``,``,
if negative.
"""
[docs] @classmethod
def read_head(cls, origin):
"""Read the octave from the token."""
return pitch.octave_from_string(origin[0].text)
[docs] def write_head(self):
"""Write the octave, an empty string for octave 0."""
return pitch.octave_to_string(self.head)
[docs]class OctCheck(element.TextElement):
"""The octavecheck after a note, e.g. like ``=,``.
The head value is the number of ``'`` (if positive) or the number of ``,``,
if negative.
"""
[docs] @classmethod
def read_head(cls, origin):
"""Read the octave from the token."""
return pitch.octave_from_string(origin[0].text[1:])
[docs] def write_head(self):
"""Write the octave, an empty string for octave 0."""
return '=' + pitch.octave_to_string(self.head)
[docs]class OctaveCheck(element.HeadElement):
r"""The ``\octaveCheck`` command."""
space_after_head = space_between = " "
head = r'\octaveCheck'
[docs] def signatures(self):
yield Pitchable,
[docs] def add_argument(self, node):
"""Reimplemented to turn an added Note argument into a Pitch."""
if isinstance(node, Note) and sum(1 for n in self / Pitchable) < 1:
node = node.to_pitch()
return super().add_argument(node)
[docs]class Duration(element.TextElement):
"""A duration after a note.
To the constructor the duration is specified using a numerical value, which
can be a :class:`~fractions.Fraction`. A whole note is 1, a breve 2, etc; a
quarter note or crotchet 1/4, etc.
The value must be expressable (is that English?:-) in a length value
and zero or more dots. Examples::
>>> from quickly.dom.lily import Duration
>>> Duration(2).write()
'\\breve'
>>> Duration(3/2).write()
'1.'
>>> Duration(7/4).write()
'1..'
>>> Duration(7/16).write()
'4..'
"""
def __init__(self, head, *children, **attrs):
if not isinstance(head, fractions.Fraction):
head = fractions.Fraction(head).limit_denominator()
super().__init__(head, *children, **attrs)
[docs] @classmethod
def from_string(cls, text):
"""Convenience constructor to make a Duration from a string.
Examples::
>>> lily.Duration.from_string('4')
<lily.Duration Fraction(1, 4)>
>>> lily.Duration.from_string('2.')
<lily.Duration Fraction(3, 4)>
>>> d = lily.Duration.from_string('1*1/3')
>>> d.dump()
<lily.Duration Fraction(1, 1) (1 child)>
╰╴<lily.DurationScaling Fraction(1, 3)>
>>> d.duration()
Fraction(1, 3)
"""
dur, *scalings = text.split('*')
scaling = 1
for t in scalings:
scaling *= fractions.Fraction(t)
return cls.from_duration(duration.from_string(dur), scaling)
[docs] @classmethod
def from_duration(cls, duration, scaling=1):
"""Convenience constructor to make a Duration from a duration and scaling value.
An example::
>>> from quickly.dom.lily import Duration
>>> d = Duration.from_duration(1/4)
>>> d.write()
'4'
>>> d = Duration.from_duration(1/4, 1/3)
>>> d.write()
'4*1/3'
"""
n = cls(duration)
if scaling != 1:
n.append(DurationScaling(scaling))
return n
[docs] def duration(self):
"""Return the two-tuple(duration, scaling).
The duration is simply our head value, the scaling is computed
from a :class:`DurationScaling` child if present, or 1.
"""
scaling = 1
for s in self / DurationScaling:
scaling *= s.head
return self.head, scaling
[docs] @classmethod
def read_head(cls, origin):
"""Read the duration value from the origin tokens."""
dur, *dots = origin
return duration.from_string(dur.text, len(dots))
[docs] def write_head(self):
"""Write back the duration fraction to a string like ``4.``"""
return duration.to_string(self.head)
[docs]class DurationScaling(element.TextElement):
"""An optional scaling after a :class:`Duration`.
E.g. ``*1/2``. May be read from multiple ``*n/m`` parts, but always outputs
a single ``*n/d`` value, or ``*n`` when the denominator is 1. To the
constructor any numerical value may be given, but the value is always
represented as a fraction (omitting the denominator if 1).
"""
def __init__(self, head, *children, **attrs):
if not isinstance(head, fractions.Fraction):
head = fractions.Fraction(head).limit_denominator()
super().__init__(head, *children, **attrs)
[docs] @classmethod
def read_head(cls, origin):
"""Read the scaling from the origin tokens."""
scaling = 1
for t in origin:
if t != "*":
scaling *= fractions.Fraction(t.text)
return scaling
[docs] def write_head(self):
"""Write back the scaling to a string like ``*1/2``."""
if self.head != 1:
return "*{}".format(fractions.Fraction(self.head))
return ""
[docs]class LyricItem(Durable):
r"""Wrap a Scheme, String, Symbol or Markup in lyricmode.
If it has no Scherm, String, Symbol or Markup child, a duration is
required.
"""
[docs] def child_order(self):
yield (Scheme, String, Symbol, Markup), Duration, base.Comment
@property
def duration_required(self):
"""Duration is required if no visible child."""
return not any(self / (Scheme, String, Symbol, Markup))
[docs]class LyricText(element.TextElement, Durable):
r"""A word in lyric mode."""
[docs] def child_order(self):
yield Duration, base.Comment
[docs]class LyricExtender(element.HeadElement):
r"""A lyric extender ``__``."""
head = "__"
# note: LilyPond >=2.20 allows a duration after a -- and __,
# but it doesn't make much sense, so we don't inherit Durable
[docs]class LyricHyphen(element.HeadElement):
r"""A lyric hyphen ``--``."""
head = "--"
# note: LilyPond >=2.20 allows a duration after a -- and __
# but it doesn't make much sense, so we don't inherit Durable
[docs]class LyricSkip(element.HeadElement, Durable):
r"""A lyric skip ``_``."""
head = "_"
[docs] def child_order(self):
yield Duration, base.Comment
[docs]class ChordModifiers(element.Element):
r"""A list of elements attachted to a note in chord mode."""
[docs]class AddSteps(element.HeadElement):
r"""Contains the steps to be added in chordmode."""
head = ":"
[docs]class RemoveSteps(element.HeadElement):
r"""Contains the steps to be added in chordmode."""
head = "^"
[docs]class Qualifier(element.TextElement):
"""A qualifier like ``maj`` in chord mode."""
[docs]class Inversion(element.HeadElement):
r"""Inversion (``/``) in chordmode."""
head = '/'
[docs]class AddInversion(element.HeadElement):
r"""Inversion adding the bass note (``/+``) in chordmode."""
head = '/+'
[docs]class Step(Int):
r"""Contains the steps to be added in chordmode."""
[docs]class Alteration(element.TextElement):
r"""The alteration of a step (``+`` or ``-``)."""
[docs]class Articulations(element.Element):
r"""A list of elements that are attached to a note or chord."""
[docs]class Direction(element.MappingElement):
r"""A ``-``, ``_`` or ``^``.
The value is -1 for ``_``, 0 for ``-`` or 1 for ``^``
"""
mapping = {
'_': -1,
'-': 0,
'^': 1,
}
[docs]class Articulation(element.TextElement):
r"""An ArticulationEvent."""
[docs]class Modifier(base.BackslashCommand):
r"""A generic modifier that is not an articulation but added to
the Articulations after a note.
For example ``\noBeam``.
The backslash is not in the head value.
"""
[docs]class RestModifier(element.HeadElement):
r"""A ``\rest`` command after a note.
Is a child of a Rest element that has a pitch name and possibly
octave information instead of plain "r".
"""
head = r'\rest'
[docs]class Fingering(element.TextElement):
r"""A FingeringEvent."""
[docs]class Dynamic(base.BackslashCommand):
r"""A dynamic symbol, like ``pp``."""
[docs]class Slur(Spanner):
r"""A slur ``(`` or ``)``."""
spanner_start = "("
spanner_stop = ")"
[docs]class PhrasingSlur(Spanner):
r"""A phrasing slur ``\(`` or ``\)``."""
spanner_start = r"\("
spanner_stop = r"\)"
[docs]class Tie(element.HeadElement):
r"""A tie."""
head = '~'
[docs]class LaissezVibrer(Tie):
r"""A ``\laissezVibrer`` tie."""
head = '\\laissezVibrer'
[docs]class RepeatTie(Tie):
r"""A ``\repeatTie`` tie."""
head = '\\repeatTie'
[docs]class Beam(Spanner):
r"""A beam ``[`` or ``]``."""
spanner_start = "["
spanner_stop = "]"
[docs]class Ligature(Spanner):
r"""A ligature ``\[`` or ``\]``."""
spanner_start = r"\["
spanner_stop = r"\]"
[docs]class TextSpanner(Spanner):
r"""A text spanner."""
spanner_start = r'\startTextSpan'
spanner_stop = r'\stopTextSpan'
[docs]class TrillSpanner(Spanner):
r"""A trill spanner."""
spanner_start = r'\startTrillSpan'
spanner_stop = r'\stopTrillSpan'
[docs]class Melisma(Spanner):
r"""A melisma spanner."""
spanner_start = r'\melisma'
spanner_stop = r'\melismaEnd'
[docs]class Arpeggio(element.HeadElement, Music):
r"""An ``\arpeggio``."""
head = r'\arpeggio'
[docs]class Glissando(element.HeadElement, Music):
r"""A ``\glissando``."""
head = r'\glissando'
[docs]class Bar(element.HeadElement, Music):
r"""A ``\bar``. Has a String child."""
space_after_head = " "
head = r'\bar'
[docs] def signatures(self):
yield String,
[docs]class Breathe(element.HeadElement, Music):
r"""A ``\breathe``."""
head = r'\breathe'
[docs]class Break(element.ToggleElement):
r"""A ``\break`` or ``\noBreak``. """
toggle_on = r'\break'
toggle_off = r'\noBreak'
[docs]class PageBreak(element.ToggleElement):
r"""A ``\pageBreak`` or ``\noPageBreak``."""
toggle_on = r'\pageBreak'
toggle_off = r'\noPageBreak'
[docs]class PageTurn(element.MappingElement):
r"""A ``\pageTurn``, ``\allowPageTurn`` or ``\noPageTurn``."""
mapping = {
r'\pageTurn': 'yes',
r'\noPageTurn': 'no',
r'\allowPageTurn': 'allow',
}
[docs]class InStaffSegno(element.HeadElement, Music):
r"""An ``\inStaffSegno`` command."""
head = r'\inStaffSegno'
[docs]class PipeSymbol(element.HeadElement):
r"""A PipeSymbol, most times used as bar check."""
head = "|"
[docs]class VoiceSeparator(element.HeadElement):
r"""A voice separator."""
head = r"\\"
[docs]class Label(element.HeadElement, Music):
r"""A ``\label`` command. Has one scheme expression child."""
head = r'\label'
[docs] def signatures(self):
yield Scheme,
[docs]class Mark(element.HeadElement, Music):
r"""A ``\mark`` command. Has one child."""
head = r'\mark'
[docs] def signatures(self):
yield Default,
yield VALUE,
[docs]class Default(element.HeadElement):
r"""The ``\default`` mark argument."""
head = r'\default'
[docs]class Tempo(_ConvertUnpitchedToDuration, HandleDuration, element.HeadElement, Music):
r"""A ``\tempo`` command.
Can have text (symbol, string, markup) child and/or duration, EqualSign and
numeric value childs.
"""
space_after_head = space_between = " "
head = r"\tempo"
[docs] def child_order(self):
yield TEXT, Duration, EqualSign, Int
[docs] def signatures(self):
yield TEXT,
yield TEXT, Unpitched, EqualSign, Int
yield Unpitched, EqualSign, Int
[docs]class SpannerId(element.HeadElement):
r"""A spanner id (``\=``).
The first child is the id (Int, Symbol, String or Scheme). The second
child the attached slur, phrasing slur or other object. (LilyPond only
supports slurs).
"""
head = r"\="
[docs]class PesOrFlexa(element.HeadElement):
r"""A pes-or-flexa event (``\~``)."""
head = r"\~"
[docs]class Tweak(element.HeadElement):
r"""A ``\tweak`` command.
On the music level, this node has two children, a Symbol and an argument.
As an event after a note, this node has three children, the symbol,
the argument and the object to tweak.
"""
space_after_head = space_between = space_after = " "
head = r'\tweak'
[docs]class Tremolo(element.HeadElement):
r"""A Tremolo (``:``) with an optional Duration child."""
head = ":"
[docs]class Mode(base.BackslashCommand):
r"""The mode subcommand of the ``\key`` statement."""
[docs]class Key(element.HeadElement, Music):
r"""A \key statement.
Must have a Pitch and a (Mode, IdentifierRef or Scheme) child.
"""
space_after_head = space_between = ' '
head = r"\key"
[docs] def key_signature(self, processor, scope=None, wait=True):
"""Return a :class:`~.key.KeySignature` object for this key signature.
The processor is a :class:`~.pitch.PitchProcessor`, which interprets
the pitch language. ``scope`` and ``wait`` help (when the mode argument
is a variable) finding its value in another file.
"""
pitch = mode = None
for n in self:
while True:
if isinstance(n, Pitchable):
pitch = processor.read_node(n)
elif isinstance(n, Mode):
mode = n.head
elif isinstance(n, IdentifierRef):
scope, n = n.get_value_with_scope(scope, wait)
continue
elif isinstance(n, Scheme):
for n in n // scm.List:
mode = [n.head for n in n / scm.Number]
break
if pitch and mode:
from ..key import KeySignature
return KeySignature(pitch.note, pitch.alter, mode)
[docs] def signatures(self):
yield Pitchable, (Mode, IdentifierRef, Scheme)
[docs] def add_argument(self, node):
"""Reimplemented to turn an added Note argument into a Pitch."""
if isinstance(node, Note) and sum(1 for n in self / Pitchable) < 1:
node = node.to_pitch()
return super().add_argument(node)
[docs]class Clef(element.HeadElement, Music):
r"""A ``\clef`` statement.
Must have a Symbol or String child indicating the clef type.
"""
space_after_head = " "
head = r"\clef"
[docs] def signatures(self):
yield (Symbol, String),
[docs]class Time(element.HeadElement, Music):
r"""A ``\time`` statement.
Has an optional List child and a Fraction child.
"""
space_after_head = " "
head = r"\time"
[docs] def signatures(self):
yield List, Fraction
yield Fraction,
[docs]class Partial(_ConvertUnpitchedToDuration, HandleDuration, element.HeadElement, Music):
r"""A ``\partial`` statement.
Has a Duration child.
"""
space_after_head = " "
head = r"\partial"
[docs] def signatures(self):
yield Duration,
yield Unpitched,
[docs]class Times(element.HeadElement, Music):
r"""A ``\times`` statement.
Has a Fraction child and a Music child.
The ``\times`` command is not documented anymore in LilyPond, but also
not deprecated. Using ``\tuplet`` is recommended.
"""
space_after_head = space_between = " "
head = r"\times"
[docs] def signatures(self):
yield Fraction, MUSIC
[docs]class Tuplet(_ConvertUnpitchedToDuration, HandleDuration, element.HeadElement, Music):
r"""A ``\tuplet`` statement.
Has a Fraction child, an optional Duration child and a Music child.
"""
space_after_head = space_between = " "
head = r"\tuplet"
[docs] def child_order(self):
yield Fraction, Duration, MUSIC
[docs] def signatures(self):
yield Fraction, Duration, MUSIC
yield Fraction, Unpitched, MUSIC
yield Fraction, MUSIC
[docs]class ScaleDurations(_ConvertUnpitchedToInt, element.HeadElement, Music):
r"""A ``\scaleDurations`` command.
Has a Fraction child and a Music child.
"""
space_after_head = space_between = " "
head = r"\scaleDurations"
[docs] def signatures(self):
yield (Fraction, Int, Unpitched, Scheme), MUSIC
[docs]class ShiftDurations(_ConvertUnpitchedToInt, element.HeadElement, Music):
r"""A ``\shiftDurations`` command.
Has two Scheme children and a Music child.
"""
space_after_head = space_between = " "
head = r"\shiftDurations"
[docs] def signatures(self):
yield NUMBER, NUMBER, MUSIC
[docs]class Grace(element.HeadElement, Music):
r"""A ``\grace`` command.
Has a Music child.
"""
space_after_head = " "
head = r"\grace"
[docs] def signatures(self):
yield MUSIC,
[docs]class Acciaccatura(Grace):
r"""An ``\acciaccatura`` command.
Has a Music child.
"""
head = r"\acciaccatura"
[docs]class Appoggiatura(Grace):
r"""An ``\appoggiatura`` command.
Has a Music child.
"""
head = r"\appoggiatura"
[docs]class SlashedGrace(Grace):
r"""A ``\slashedGrace`` command.
Has a Music child.
"""
head = r"\slashedGrace"
[docs]class AfterGrace(element.HeadElement, Music):
r"""An ``\afterGrace`` command.
Has an optional Fraction and two Music children. The second music
expression is the grace music and has length 0, the fraction is multiplied
with the duration of the first music expression and determines the moment
the grace music is displayed.
The default fraction (if not specified) is in the toplevel
``afterGraceFraction`` variable or 3/4.
"""
space_after_head = space_between = " "
head = r"\afterGrace"
[docs] def time_length(self, context, end=None):
"""Return the length of this expression, using a :class:`~.time.TimeContext`
handler.
Reimplemented to skip the second child music expression.
"""
for n in self[:end]:
if isinstance(n, Music):
return context.length(n)
return 0
[docs] def signatures(self):
yield Fraction, MUSIC, MUSIC
yield MUSIC, MUSIC
[docs]class PartCombine(element.HeadElement, Music):
r"""The ``\partcombine`` command, with two Music arguments."""
space_after_head = space_between = " "
head = r"\partcombine"
[docs] def signatures(self):
yield MUSIC, MUSIC
[docs]class Markup(base.BackslashCommand):
r"""A ``\markup``, ``\markuplines`` or ``\markuplist`` expression.
When manually constructing a Markup, the backslash is not needed.
"""
space_before = ""
space_after = " "
space_between = space_after_head = " "
[docs]class MarkupWord(element.TextElement):
"""A word in markup mode."""
space_before = space_after = " "
[docs]class MarkupList(element.BlockElement):
"""A bracketed markup expression, like ``{`` ... ``}``."""
space_after_head = space_before_tail = space_between = " "
head = "{"
tail = "}"
[docs] def indent_align_indices(self):
"""How to align child nodes if on the same line as an indenting char."""
yield 0
[docs]class MarkupCommand(base.BackslashCommand, Reference):
r"""A markup command, like ``\bold <arg>``.
When manually constructing a MarkupCommand, the backslash is not needed.
"""
space_after_head = space_before_tail = space_between = " "
[docs] def get_name(self):
"""The name of the markup command. This is used when looking up
the definition of a custom markup command.
"""
return self.head
[docs]class MarkupScore(Score):
r"""A ``\score`` in Markup."""
space_after_head = space_before_tail = space_between = " "
[docs]class MarkupScoreLines(MarkupScore):
r"""A ``\score-lines`` in Markup."""
head = r"\score-lines {"
[docs]class FigureBody(element.BlockElement):
r"""One ``<`` ... ``>`` figure "chord" in figuremode.
Always the child of a Figure element, which can have a duration.
"""
space_between = " "
head = '<'
tail = '>'
[docs]class Tag(element.HeadElement):
r"""A ``\tag`` command."""
head = r'\tag'
[docs] def signatures(self):
yield (Symbol, String, Scheme), MUSIC
[docs]class KeepWithTag(element.HeadElement):
r"""A ``\keepWithTag`` command."""
head = r'\keepWithTag'
[docs] def signatures(self):
yield (Symbol, String, Scheme), MUSIC
[docs]class RemoveWithTag(element.HeadElement):
r"""A ``\removeWithTag`` command."""
head = r'\removeWithTag'
[docs] def signatures(self):
yield (Symbol, String, Scheme), MUSIC
[docs]class TagGroup(element.HeadElement):
r"""A ``\tagGroup`` command."""
head = r'\tagGroup'
[docs] def signatures(self):
yield (Symbol, String, Scheme),
[docs]class PushToTag(element.HeadElement):
r"""A ``\pushToTag`` command."""
head = r'\pushToTag'
[docs] def signatures(self):
yield (Symbol, String, Scheme), MUSIC
[docs]class AppendToTag(element.HeadElement):
r"""A ``\appendToTag`` command."""
head = r'\appendToTag'
[docs] def signatures(self):
yield (Symbol, String, Scheme), MUSIC
[docs]class Etc(element.HeadElement):
r"""The ``\etc`` placeholder."""
head = r'\etc'
[docs]class Accepts(element.HeadElement):
r"""The ``\accepts`` command."""
space_after_head = space_between = " "
head = r'\accepts'
[docs] def signatures(self):
yield (String, Symbol),
[docs]class Denies(element.HeadElement):
r"""The ``\denies`` command."""
space_after_head = space_between = " "
head = r'\denies'
[docs] def signatures(self):
yield (String, Symbol),
[docs]class Name(element.HeadElement):
r"""The ``\name`` command."""
space_after_head = space_between = " "
head = r'\name'
[docs] def signatures(self):
yield (String, Symbol),
[docs]class Alias(element.HeadElement):
r"""The ``\alias`` command."""
space_after_head = space_between = " "
head = r'\alias'
[docs] def signatures(self):
yield (String, Symbol),
[docs]class Consists(element.HeadElement):
r"""The ``\consists`` command."""
space_after_head = space_between = " "
head = r'\consists'
[docs] def signatures(self):
yield (String, Symbol, Scheme),
[docs]class Remove(element.HeadElement):
r"""The ``\remove`` command."""
space_after_head = space_between = " "
head = r'\remove'
[docs] def signatures(self):
yield (String, Symbol, Scheme),
[docs]class DefaultChild(element.HeadElement):
r"""The ``\defaultchild`` command."""
space_after_head = space_between = " "
head = r'\defaultchild'
[docs] def signatures(self):
yield (String, Symbol),
[docs]class Omit(element.HeadElement, Music):
r"""The ``\omit`` command."""
space_after_head = space_between = " "
head = r'\omit'
[docs] def signatures(self):
yield SYMBOL,
[docs]class Hide(element.HeadElement, Music):
r"""The ``\hide`` command."""
space_after_head = space_between = " "
head = r'\hide'
[docs] def signatures(self):
yield SYMBOL,
[docs]class Undo(element.HeadElement, Music):
r"""The ``\undo`` command."""
space_after_head = space_between = " "
head = r'\undo'
[docs] def signatures(self):
yield MUSIC,
[docs]class Once(element.HeadElement, Music):
r"""The ``\once`` command."""
space_after_head = space_between = " "
head = r'\once'
[docs] def signatures(self):
yield MUSIC,
[docs]class Temporary(element.HeadElement, Music):
r"""The ``\temporary`` command."""
space_after_head = space_between = " "
head = r'\temporary'
[docs] def signatures(self):
yield MUSIC,
[docs]class Override(_ConvertUnpitchedToInt, element.HeadElement, Music):
r"""The ``\override`` command."""
space_after_head = space_between = " "
head = r'\override'
[docs] def signatures(self):
yield SYMBOL, EqualSign, VALUE
yield SYMBOL, Scheme, EqualSign, VALUE
yield SYMBOL, Scheme, Scheme, EqualSign, VALUE
yield SYMBOL, Scheme, Scheme, Scheme, EqualSign, VALUE
yield SYMBOL, Scheme, Scheme, Scheme, Scheme, EqualSign, VALUE
[docs]class Revert(element.HeadElement, Music):
r"""The ``\revert`` command."""
space_after_head = space_between = " "
head = r'\revert'
[docs] def signatures(self):
yield SYMBOL,
yield SYMBOL, Scheme
yield SYMBOL, Scheme, Scheme
yield SYMBOL, Scheme, Scheme, Scheme
yield SYMBOL, Scheme, Scheme, Scheme, Scheme
[docs]class Set(_ConvertUnpitchedToInt, element.HeadElement, Music):
r"""The ``\set`` command."""
space_after_head = space_between = " "
head = r'\set'
[docs] def signatures(self):
yield SYMBOL, EqualSign, VALUE
[docs]class Unset(element.HeadElement, Music):
r"""The ``\unset`` command."""
space_after_head = space_between = " "
head = r'\unset'
[docs] def signatures(self):
yield SYMBOL,
[docs]class Version(element.HeadElement, Music):
r"""The ``\version`` command."""
space_after_head = space_between = " "
head = r'\version'
@property
def version(self):
"""The version number, as a tuple of ints (may be empty)."""
for s in self/String:
return tuple(map(int, re.findall(r'\d+', s.head)))
return ()
@version.setter
def version(self, version):
version = '.'.join(map(str, version))
for s in self/String:
s.head = version
break
else:
self.insert(0, String(version))
[docs] def signatures(self):
yield String,
[docs]class Language(element.HeadElement):
r"""The ``\language`` command.
Has a :class:`String` child with the language name, which can be
conveniently edited via the :attr:`language` attribute.
"""
space_after_head = space_between = " "
head = r'\language'
[docs] def signatures(self):
yield String,
[docs] def repr_head(self):
return repr(self.language)
@property
def language(self):
"""The language."""
for n in self / String:
return n.head
@language.setter
def language(self, language):
for n in self / String:
n.head = language
return
self.insert(0, String(language))
[docs]class Include(element.HeadElement):
r"""The ``\include`` command.
You can use the :attr:``language`` attribute, assuming that the included
file is a language definition file. If the :attr:`filename` is not
recognized as a language definition file, the property will return None.
"""
space_after_head = space_between = " "
head = r'\include'
[docs] def signatures(self):
yield String,
[docs] def repr_head(self):
return repr(self.filename)
@property
def filename(self):
"""The filename."""
for n in self / String:
return n.head
@filename.setter
def filename(self, value):
for n in self / String:
n.head = value
return
self.insert(0, String(value))
@property
def language(self):
"""The language, if the filename refers to a known language definition.
Setting the attribute appends the ``".ly"`` automatically.
"""
from .. import pitch
filename = self.filename
if filename.endswith(".ly"):
language = filename[:-3]
if language in pitch.pitch_names:
return language
@language.setter
def language(self, language):
self.filename = language + ".ly"
[docs]class VoiceN(element.MappingElement):
r"""Commands like ``\voiceOne``, ``\voiceTwo``, etc."""
mapping = {
r'\voiceOne': 1,
r'\voiceTwo': 2,
r'\voiceThree': 3,
r'\voiceFour': 4,
r'\voiceFive': 5,
r'\voiceSix': 6,
}
[docs]class GrobDirection(element.MappingElement):
r"""A collection of commands concerning direction, like ``\slurUp``.
To create a ``\slurUp`` command, use::
>>> node = lily.GrobDirection(("Slur", 1))
>>> node.write()
'\\slurUp'
Or::
>>> node = lily.GrobDirection.from_mapping(r'\slurUp')
When reading this node programmatically, the ``grob`` and ``direction``
attributes can be read and modified::
>>> node.grob
'Slur'
>>> node.direction
1
>>> node.write()
'\\slurUp'
>>> node.direction = -1
>>> node.write()
'\\slurDown'
The ``grobs`` class attribute is a dictionary mapping each available grob
to a tuple of the directions it supports. (Technically not all named
objects are grobs (graphical objects).) Most "grobs" support all three
directions: Up (1), Neutral (0), Down (-1).
"""
mapping = {
r'\arpeggioArrowDown': ('ArpeggioArrow', -1),
r'\arpeggioArrowUp': ('ArpeggioArrow', 1),
r'\bassFigureStaffAlignmentDown': ('BassFigureStaffAlignment', -1),
r'\bassFigureStaffAlignmentNeutral': ('BassFigureStaffAlignment', 0),
r'\bassFigureStaffAlignmentUp': ('BassFigureStaffAlignment', 1),
r'\dotsDown': ('Dots', -1),
r'\dotsNeutral': ('Dots', 0),
r'\dotsUp': ('Dots', 1),
r'\dynamicDown': ('Dynamic', -1),
r'\dynamicNeutral': ('Dynamic', 0),
r'\dynamicUp': ('Dynamic', 1),
r'\phrasingSlurDown': ('PhrasingSlur', -1),
r'\phrasingSlurNeutral': ('PhrasingSlur', 0),
r'\phrasingSlurUp': ('PhrasingSlur', 1),
r'\slurDown': ('Slur', -1),
r'\slurNeutral': ('Slur', 0),
r'\slurUp': ('Slur', 1),
r'\stemDown': ('Stem', -1),
r'\stemNeutral': ('Stem', 0),
r'\stemUp': ('Stem', 1),
r'\textSpannerDown': ('TextSpanner', -1),
r'\textSpannerNeutral': ('TextSpanner', 0),
r'\textSpannerUp': ('TextSpanner', 1),
r'\tieDown': ('Tie', -1),
r'\tieNeutral': ('Tie', 0),
r'\tieUp': ('Tie', 1),
r'\tupletDown': ('Tuplet', -1),
r'\tupletNeutral': ('Tuplet', 0),
r'\tupletUp': ('Tuplet', 1),
}
def grobs(mapping):
d = collections.defaultdict(list)
for grob, direction in mapping.values():
d[grob].append(direction)
return {grob: tuple(sorted(dirs)) for grob, dirs in d.items()}
grobs = grobs(mapping)
@property
def grob(self):
"""The grob (graphical object), starting with a Capital."""
return self.head[0]
@grob.setter
def grob(self, value):
self.head = (value, self.head[1])
@property
def direction(self):
"""The direction, 1 for up, 0 for neutral, -1 for down."""
return self.head[1]
@direction.setter
def direction(self, value):
self.head = (self.head[0], value)
[docs]class GrobStyle(element.MappingElement):
r"""A collection of commands concerning direction, like ``\slurDashed``.
To create a ``\slurDashed`` command, use::
>>> node = lily.GrobStyle(("Slur", "dashed"))
>>> node.write()
'\\slurDashed'
Or::
>>> node = lily.GrobStyle.from_mapping(r'\slurDashed')
When reading this node programmatically, the ``grob`` and ``style``
attributes can be read and modified::
>>> node.grob
'Slur'
>>> node.style
'dashed'
>>> node.write()
'\\slurDashed'
>>> node.style = "dotted"
>>> node.write()
'\\slurDotted'
The ``grobs`` class attribute is a dictionary mapping each available grob
to a tuple of the styles it supports. All grobs support the styles
``"solid"``, ``"dashed"``, and ``"dotted"``.
"""
mapping = {
r'\phrasingSlurDashed': ('PhrasingSlur', "dashed"),
r'\phrasingSlurDotted': ('PhrasingSlur', "dotted"),
r'\phrasingSlurSolid': ('PhrasingSlur', "solid"),
r'\slurDashed': ('Slur', "dashed"),
r'\slurDotted': ('Slur', "dotted"),
r'\slurSolid': ('Slur', "solid"),
r'\tieDashed': ('Tie', "dashed"),
r'\tieDotted': ('Tie', "dotted"),
r'\tieSolid': ('Tie', "solid"),
}
def grobs(mapping):
d = collections.defaultdict(list)
for grob, direction in mapping.values():
d[grob].append(direction)
return {grob: tuple(sorted(dirs)) for grob, dirs in d.items()}
grobs = grobs(mapping)
@property
def grob(self):
"""The grob (graphical object), starting with a Capital."""
return self.head[0]
@grob.setter
def grob(self, value):
self.head = (value, self.head[1])
@property
def style(self):
"""The style: ``"solid"``, ``"dashed"``, or ``"dotted"``."""
return self.head[1]
@style.setter
def style(self, value):
self.head = (self.head[0], value)
[docs]class Toggle(element.MappingElement):
r"""A collection of commands that can be on/off, like ``\textLengthOn``.
To create a ``\textLengthOn`` command, use::
>>> node = lily.Toggle(("textLength", True))
>>> node.write()
'\\textLengthOn'
Or::
>>> node = lily.Toggle.from_mapping(r'\textLengthOn')
When reading this node programmatically, the ``prop`` and ``value``
attributes can be read and modified::
>>> node.prop
'textLength'
>>> node.value
True
>>> node.write()
'\\textLengthOn'
>>> node.value = False
>>> node.write()
'\\textLengthOff'
The ``props`` class attribute is a tuple with all available prop names. All
props support the values True and False.
"""
props = (
"autoBeam",
"autoBreaks", # since 2.20
"autoLineBreaks", # since 2.20
"autoPageBreaks", # since 2.20
"balloonLength",
"bassFigureExtenders",
"cadenza",
"deadNotes", # since 2.20
"easyHeads",
"improvisation",
"harmonics",
"kievan", # since 2.20
"markLength", # since 2.18
"mergeDifferentlyDotted",
"mergeDifferentlyHeaded",
"pointAndClick",
"predefinedFretboards",
"sostenuto",
"sustain",
"textLength",
"xNotes",
)
mapping = {}
for p in props:
mapping[r'\{}On'.format(p)] = (p, True)
mapping[r'\{}Off'.format(p)] = (p, False)
del p
@property
def prop(self):
"""The property that can be on or off."""
return self.head[0]
@prop.setter
def prop(self, value):
self.head = (value, self.head[1])
@property
def value(self):
"""The value (True for ``"On"``, False for ``"Off"``)."""
return self.head[1]
@value.setter
def value(self, value):
self.head = (self.head[0], value)
[docs]class Shape(element.HeadElement):
r"""The ``\shape`` command.
Has a Scheme and a SYMBOL child. (As articulation, has only a
Scheme child.)
"""
space_after_head = space_between = " "
head = r'\shape'
[docs] def signatures(self):
yield Scheme, SYMBOL
[docs]class VShape(Shape):
r"""The ``\vshape`` command.
Like ``\shape`` but draws the control points.
"""
head = r'\vshape'
[docs]class StringTuning(element.HeadElement):
r"""The ``\stringTuning`` command, with one Chord argument."""
space_after_head = space_between = " "
head = r'\stringTuning'
[docs] def signatures(self):
yield MUSIC,
[docs]class Unit(element.MappingElement):
r"""A unit: ``\mm``, ``\cm``, ``\pt`` or ``\in``.
A Unit node can be attached to an Int or Float::
>>> from quickly.dom import lily
>>> lily.Int(10, lily.Unit("cm")).write()
'10\\cm'
"""
mapping = {
r'\mm': 'mm',
r'\cm': 'cm',
r'\in': 'in',
r'\pt': 'pt',
}
### Helper classes and functions
[docs]class Lookup:
"""Helper class to find definitions and other stuff in a lily.Document from
the viewpoint of a ``node``.
A :class:`~.scope.Scope`, if given using the ``scope`` parameter, is used
to resolve include files. If no scope is given, only searches the current
DOM document; the yielded scope is then always None.
If a scope is given, include commands are followed and ``wait`` determines
whether to wait for ongoing transformations of external DOM documents. If
wait is False, and a transformation is not yet finished the included
document's toplevel nodes will not be yielded.
"""
def __init__(self, node, scope=None, wait=True):
self.node = node
self.scope = scope
self.wait = wait
def __repr__(self):
return "<{} node={} scope={} wait={}>".format(
type(self).__name__, self.node, self.scope, self.wait)
[docs] def ancestors(self):
"""Yield the ancestors with index of node that should be searched for
possible definitions.
The default implementation yields ancestors that inherit
HandleAssignments and stops at the Document node.
"""
for node, index in self.node.ancestors_with_index():
if isinstance(node, HandleAssignments):
yield node, index
if isinstance(node, Document):
break
[docs] def preceding_nodes(self):
r"""Yield preceding nodes in ancestors and toplevel, in backward
direction, as two-tuples (node, scope).
If a scope was given on instantiation, included files are followed.
When the beginning of the current scope (document) is reached,
continues the search in parent scopes (if a scope was given), and if
that parent scope has a node pointer, continues the search from there.
This is useful when an included file refers to a variable that was set
in the parent document before the ``\include`` command.
So when ``file_a.ly`` reads::
music = { c }
\include "file_b.ly"
And ``file_b.ly`` only contains::
\new Score { \music }
When traversing the musical contents of file a, the value of the
``\music`` Reference in file b is correctly found in file a.
"""
scope = self.scope
if scope:
lookup = self
while True:
for p, i in lookup.ancestors():
stack = []
gen = reversed(p[:i])
while True:
for n in gen:
if isinstance(n, Include):
new_scope = scope.include_scope(n.filename, n)
if new_scope:
dom = new_scope.document().get_transform(lookup.wait)
if isinstance(dom, Document):
stack.append((scope, gen))
scope = new_scope
gen = reversed(dom)
break
yield n, scope
else:
if stack:
scope, gen = stack.pop()
else:
break
if scope.parent and scope.node:
lookup = type(self)(scope.node, scope.parent, self.wait)
scope = scope.parent
else:
break
else:
for p, i in self.ancestors():
for n in reversed(p[:i]):
yield n, None
[docs] def find_assignment(self, name):
"""Find an Assignment from here, with name ``name``.
If found, return its value and the scope.
"""
for node, scope in self.preceding_nodes():
if isinstance(node, Assignment) and node.get_name() == name:
return node.get_value(), scope
[docs]def is_music(node):
"""Return True if the node is an instance of Music."""
return isinstance(node, Music)
[docs]def is_symbol(text):
"""Return True if text is a valid LilyPond symbol."""
from parce.lang import lilypond, lilypond_words
return (re.fullmatch(lilypond.RE_LILYPOND_SYMBOL, text) and
text not in lilypond_words.all_pitch_names)
[docs]def make_list_node(value):
"""Return an element node corresponding to the value.
If value is a string, a Symbol is returned if it's valid LilyPond identifier.
otherwise String. If value is an integer, a Int is returned.
If no suitable node type could be returned, None is returned.
"""
if isinstance(value, str):
return Symbol(value) if is_symbol(value) else String(value)
elif isinstance(value, int):
return Int(value)
[docs]def duration_getter():
"""Return a callable that returns the (duration, scaling) tuple of a
:class:`Durable`.
If the durable does not have a Duration child, the callable searches
backwards until a Durable is found that has a duration that LilyPond would
use for the current durable. All found durables without duration are
cached, so the next request only needs at most to search back one durable.
If no durable with a value is found, ``(Fraction(1, 4), 1)`` is returned.
Use this getter if you are not sure you really iterate over all the
durables in a node and cannot keep track of the previous durable yourself.
Get a new getter if you also modify durations.
"""
durables = set()
duration = None
def get_duration(durable):
"""Return the tuple (duration, scaling) of the Durable."""
nonlocal durables, duration
dur = durable.duration_scaling
if dur:
return dur
elif durable in durables:
return duration
new = {durable}
for prev in durable < Durable:
if prev.duration_sets_previous:
dur = prev.duration_scaling
if dur:
durables = new
duration = dur
return dur
elif prev in durables:
durables.update(new)
return duration
new.add(prev)
durables = new
duration = (fractions.Fraction(1, 4), 1)
return duration
return get_duration
[docs]def previous_duration(node):
"""Return a two-tuple(duration, scaling) of the closest preceding Durable
that has a duration that LilyPond would use if the current node had no
duration.
If no such node was found, returns ``(Fraction(1, 4), 1)``.
This function is potentially slow, as it searches backwards for a Durable
node. Don't use it if you have the opportunity to keep track of the
previous duration yourself (from a :class:`Durable` that has the
:attr:`~Durable.duration_sets_previous` attribute set to True).
You can also use a :func:`duration_getter`, which optimizes for adjacent
notes without duration.
"""
for n in node < Durable:
if n.duration_sets_previous:
for d in n / Duration:
return d.duration()
return fractions.Fraction(1, 4), 1
[docs]def convert_duration_to_int(node):
"""Return an Int element, created from the specified Duration element.
This can be used if a music function wants an integer number, while the
origin token was seen as a duration by parce and the music builder.
"""
try:
try:
origin = node.head_origin
except AttributeError:
return Int(int(node.write()))
else:
return Int.with_origin(origin)
except ValueError:
return # cannot convert
[docs]def create_element_from_value(value):
"""Convert a regular Python value to a lilypond Element node.
This can be used to ease manually building a node structure. Converts:
* bool to Scheme('#', scm.Bool(value))
* int to Int(value)
* float to Float(value)
* str to String(value)
* tuple(int, "unit") to Int(value, Unit("unit")) where unit in "mm", "cm", "in", "pt"
* tuple(float, "unit") to Float(value, Unit("unit")) where unit in "mm", "cm", "in", "pt"
* an Element node is returned unchanged.
Raises ValueError when a value can't be converted to an element.
"""
if isinstance(value, element.Element):
return value
if (isinstance(value, tuple) and len(value) == 2
and isinstance(value[0], (int, float)) and value[1] in ('mm', 'cm', 'in', 'pt')):
unit = Unit(value[1]),
value = value[0]
else:
unit = ()
if isinstance(value, bool):
return Scheme('#', scm.Bool(value))
elif isinstance(value, int):
return Int(value, *unit)
elif isinstance(value, float):
return Float(value, *unit)
elif isinstance(value, str):
return String(value)
raise ValueError("Can't convert value to Element node: {}".format(repr(value)))
[docs]def create_value_from_element(node):
"""Get the Python value from an Element node.
Returns:
* bool, int, float etc for a Scheme('#', scm.Bool(value) or scm.Number(value)) node
* int for an Int node
* float for a Float node
* Fraction for a Fraction node
* str for a String, Symbol or scm.String node
* tuple(int, "unit") for a Int(Unit()) node
* tuple(float, "unit") for a Float(Unit()) node
Returns None when this function cannot get a simple value from the node.
"""
if isinstance(node, (Int, Float)):
value = node.head
for unit in node/Unit:
return (value, unit.head)
return value
elif isinstance(node, (String, Symbol)):
return node.head
elif isinstance(node, Fraction):
return node.fraction()
elif isinstance(node, Scheme):
if len(node) == 1:
node = node[0]
if isinstance(node, (scm.Bool, scm.Number, scm.String)):
return node.value
[docs]def get_num_value(node):
"""Get a numerical value from a node, if possible, else None."""
if isinstance(node, (Int, Float)):
return node.head
elif isinstance(node, Fraction):
return node.fraction()
elif isinstance(node, Scheme):
if len(node) == 1:
if isinstance(node[0], scm.Number):
return node[0].head
[docs]def get_int_value(node):
"""Get a integer value from a node, if possible, else None."""
if isinstance(node, Int):
return node.head
elif isinstance(node, Float) and node.head.is_integer():
return int(node.head)
elif isinstance(node, Scheme):
if len(node) == 1:
if isinstance(node[0], scm.Number):
v = node[0].head
if isinstance(v, int):
return v
if isinstance(v, float) and v.is_integer():
return int(v)
[docs]def filter_map(func, iterable):
"""Call func on every item in iterable and yield the result value if not
None.
Equivalent to::
filter(lambda r: r is not None, map(func, iterable))
"""
return filter(lambda r: r is not None, map(func, iterable))
# often used signatures:
MUSIC = (Music, IdentifierRef, Etc)
VALUE = (List, String, Scheme, Number, Markup, IdentifierRef, Etc, Unpitched)
SYMBOL = (List, Symbol, String)
TEXT = (List, Symbol, String, Markup, IdentifierRef, Etc)
NUMBER = (Scheme, Number, Unpitched)
INT = (Int, IdentifierRef, Scheme)