"""
Classes to work with Abjad tags.
"""
import dataclasses
import typing
from . import _indentlib
from . import string as _string
[docs]
@dataclasses.dataclass(frozen=True, order=True, slots=True, unsafe_hash=True)
class Tag:
"""
Tag.
.. container:: example
>>> abjad.Tag("YELLOW")
Tag(string='YELLOW')
>>> abjad.Tag("YELLOW:RED")
Tag(string='YELLOW:RED')
Raises exception on duplicate words in tag:
>>> abjad.Tag("YELLOW:RED:RED")
Traceback (most recent call last):
...
Exception: duplicate words in tag: 'YELLOW:RED:RED'
Raises exception on multiple only-edition tags:
>>> abjad.Tag("+SEGMENT:+PARTS")
Traceback (most recent call last):
...
Exception: at most one only-edition tag: ['+SEGMENT', '+PARTS'].
Raises exception on mixed only-edition / not-edition tags:
>>> abjad.Tag("+SEGMENT:-PARTS")
Traceback (most recent call last):
...
Exception: only-edition and not-edition forbidden in same tag:
<BLANKLINE>
['+SEGMENT'] / ['-PARTS']
"""
string: str = ""
def __post_init__(self):
assert isinstance(self.string, str), repr(self.string)
self.words()
[docs]
def append(self, word: "Tag") -> "Tag":
"""
Appends ``word`` to tag.
.. container:: example
>>> abjad.Tag("-PARTS").append(abjad.Tag("DEFAULT_CLEF"))
Tag(string='-PARTS:DEFAULT_CLEF')
"""
if not bool(word.string):
return Tag(self.string)
assert isinstance(word, Tag), repr(word)
words = []
if self.string:
words.append(self.string)
if word.string in self.words():
raise Exception(f"{word} duplicates {self}.")
words.append(word.string)
string = ":".join(words)
return Tag(string)
[docs]
def editions(self) -> list["Tag"]:
"""
Gets edition tags in tag.
.. container:: example
>>> abjad.Tag("FOO").editions()
[]
>>> abjad.Tag("+SEGMENT").only_edition()
Tag(string='+SEGMENT')
>>> abjad.Tag("+SEGMENT:FOO").only_edition()
Tag(string='+SEGMENT')
>>> abjad.Tag("-SEGMENT").editions()
[Tag(string='-SEGMENT')]
>>> abjad.Tag("-SEGMENT:FOO").editions()
[Tag(string='-SEGMENT')]
>>> abjad.Tag("-SEGMENT:-PARTS").editions()
[Tag(string='-SEGMENT'), Tag(string='-PARTS')]
"""
result = []
for word in self.words():
if word.startswith("+") or word.startswith("-"):
result.append(Tag(word))
return result
[docs]
def not_editions(self) -> list["Tag"]:
"""
Gets not-edition tags in tag.
.. container:: example
>>> abjad.Tag("FOO").not_editions()
[]
>>> abjad.Tag("-SEGMENT").not_editions()
[Tag(string='-SEGMENT')]
>>> abjad.Tag("-SEGMENT:FOO").not_editions()
[Tag(string='-SEGMENT')]
>>> abjad.Tag("-SEGMENT:-PARTS").not_editions()
[Tag(string='-SEGMENT'), Tag(string='-PARTS')]
"""
result = []
for word in self.words():
if word.startswith("-"):
result.append(Tag(word))
return result
[docs]
def only_edition(self) -> typing.Optional["Tag"]:
"""
Gets only-edition tag in tag.
.. container:: example
>>> abjad.Tag("FOO").only_edition() is None
True
>>> abjad.Tag("+SEGMENT").only_edition()
Tag(string='+SEGMENT')
>>> abjad.Tag("+SEGMENT:FOO").only_edition()
Tag(string='+SEGMENT')
"""
for word in self.words():
if word.startswith("+"):
return Tag(word)
else:
return None
[docs]
def retain_shoutcase(self) -> "Tag":
"""
Retains shoutcase.
.. container:: example
>>> tag = abjad.Tag("-PARTS:DEFAULT_CLEF:_apply_clef()")
>>> tag.retain_shoutcase()
Tag(string='-PARTS:DEFAULT_CLEF')
>>> tag = abjad.Tag("_debug_function()")
>>> tag.retain_shoutcase()
Tag(string='')
"""
words = []
for word in self.words():
if _string.is_shout_case(word) or word[0] in ("-", "+"):
words.append(word)
string = ":".join(words)
return type(self)(string)
[docs]
def words(self) -> list[str]:
"""
Gets words.
.. container:: example
>>> abjad.Tag("-PARTS:DEFAULT_CLEF").words()
['-PARTS', 'DEFAULT_CLEF']
"""
assert not self.string.startswith(":"), repr(self.string)
words = self.string.split(":")
assert isinstance(words, list), repr(words)
words_ = []
for word in words:
if word in words_:
raise Exception(f"duplicate words in tag: {self.string!r}")
words_.append(word)
only_edition_tags, not_edition_tags = [], []
for word_ in words_:
if word_.startswith("+"):
only_edition_tags.append(word_)
if word_.startswith("-"):
not_edition_tags.append(word_)
if 1 < len(only_edition_tags):
raise Exception(f"at most one only-edition tag: {only_edition_tags!r}.")
if only_edition_tags and not_edition_tags:
message = "only-edition and not-edition forbidden in same tag:\n\n"
message += f" {only_edition_tags} / {not_edition_tags}"
raise Exception(message)
assert all(isinstance(_, str) for _ in words_), repr(words_)
return words_
def _match_line(line, tag, current_tags):
assert all(isinstance(_, Tag) for _ in current_tags), repr(current_tags)
if tag in current_tags:
return True
if callable(tag):
return tag(current_tags)
assert isinstance(tag, Tag), repr(tag)
return False
[docs]
def activate(text: str, tag: Tag | typing.Callable) -> tuple[str, int, int]:
r"""
Activates ``tag`` in ``text``.
Tags can toggle indefinitely.
Returns (text, count, skipped) triple.
Count gives number of activated tags.
Skipped gives number of skipped tags.
.. container:: example
Writes (deactivated) tag with ``"%@%"`` prefix into LilyPond input:
>>> staff = abjad.Staff("c'4 d' e' f'")
>>> markup = abjad.Markup(r"\markup { \with-color #red Allegro }")
>>> abjad.attach(
... markup,
... staff[0],
... deactivate=True,
... tag=abjad.Tag("RED_MARKUP"),
... )
>>> text = abjad.lilypond(staff, tags=True)
>>> text = abjad.tag.left_shift_tags(text)
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
%@% - \markup { \with-color #red Allegro }
d'4
e'4
f'4
}
>>> abjad.show(staff) # doctest: +SKIP
Activates tag:
>>> text, count, skipped = abjad.activate(text, abjad.Tag("RED_MARKUP"))
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
- \markup { \with-color #red Allegro } %@%
d'4
e'4
f'4
}
>>> lines = [_.strip("\n") for _ in text.split("\n")]
>>> string = "\n".join(lines)
>>> lilypond_file = abjad.LilyPondFile([string])
>>> abjad.show(lilypond_file) # doctest: +SKIP
Deactivates tag again:
>>> text, count, skipped = abjad.deactivate(text, abjad.Tag("RED_MARKUP"))
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
%@% - \markup { \with-color #red Allegro }
d'4
e'4
f'4
}
>>> lines = [_.strip("\n") for _ in text.split("\n")]
>>> string = "\n".join(lines)
>>> lilypond_file = abjad.LilyPondFile([string])
>>> abjad.show(lilypond_file) # doctest: +SKIP
Activates tag again:
>>> text, count, skipped = abjad.activate(text, abjad.Tag("RED_MARKUP"))
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
- \markup { \with-color #red Allegro } %@%
d'4
e'4
f'4
}
>>> lines = [_.strip("\n") for _ in text.split("\n")]
>>> string = "\n".join(lines)
>>> lilypond_file = abjad.LilyPondFile([string])
>>> abjad.show(lilypond_file) # doctest: +SKIP
"""
assert isinstance(text, str), repr(text)
assert isinstance(tag, Tag) or callable(tag), repr(tag)
lines: list[str] = []
count, skipped_count = 0, 0
treated_last_line = False
found_already_active_on_last_line = False
text_lines = text.split("\n")
text_lines = [_ + "\n" for _ in text_lines[:-1]] + text_lines[-1:]
lines = []
current_tags = []
for line in text_lines:
if line.lstrip().startswith("%! "):
lines.append(line)
current_tag = Tag(line.strip()[3:])
current_tags.append(current_tag)
continue
if not _match_line(line, tag, current_tags):
lines.append(line)
treated_last_line = False
found_already_active_on_last_line = False
current_tags = []
continue
first_nonwhitespace_index = len(line) - len(line.lstrip())
index = first_nonwhitespace_index
if line[index : index + 4] in ("%%% ", "%@% "):
if "%@% " in line:
line = line.replace("%@% ", "")
suffix = " %@%"
else:
# TODO: replace with "" instead of " "?
line = line.replace("%%%", " ")
suffix = None
assert line.endswith("\n"), repr(line)
if suffix:
line = line.strip("\n") + suffix + "\n"
if not treated_last_line:
count += 1
treated_last_line = True
found_already_active_on_last_line = False
else:
if not found_already_active_on_last_line:
skipped_count += 1
found_already_active_on_last_line = True
treated_last_line = False
lines.append(line)
current_tags = []
text = "".join(lines)
return text, count, skipped_count
[docs]
def deactivate(
text: str,
tag: Tag | typing.Callable,
prepend_empty_chord: bool = False,
) -> tuple[str, int, int]:
r"""
Deactivates ``tag`` in ``text``.
.. container:: example
Writes (active) tag into LilyPond input:
>>> staff = abjad.Staff("c'4 d' e' f'")
>>> string = r"\markup { \with-color #red Allegro }"
>>> markup = abjad.Markup(string)
>>> abjad.attach(
... markup,
... staff[0],
... tag=abjad.Tag("RED_MARKUP"),
... )
>>> text = abjad.lilypond(staff, tags=True)
>>> text = abjad.tag.left_shift_tags(text)
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
- \markup { \with-color #red Allegro }
d'4
e'4
f'4
}
>>> abjad.show(staff) # doctest: +SKIP
Deactivates tag:
>>> text = abjad.lilypond(staff, tags=True)
>>> text, count, skipped = abjad.deactivate(text, abjad.Tag("RED_MARKUP"))
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
%%% - \markup { \with-color #red Allegro }
d'4
e'4
f'4
}
>>> lines = [_.strip("\n") for _ in text.split("\n")]
>>> string = "\n".join(lines)
>>> lilypond_file = abjad.LilyPondFile([string])
>>> abjad.show(lilypond_file) # doctest: +SKIP
Activates tag again:
>>> text, count, skipped = abjad.activate(text, abjad.Tag("RED_MARKUP"))
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
- \markup { \with-color #red Allegro }
d'4
e'4
f'4
}
>>> lines = [_.strip("\n") for _ in text.split("\n")]
>>> string = "\n".join(lines)
>>> lilypond_file = abjad.LilyPondFile([string])
>>> abjad.show(lilypond_file) # doctest: +SKIP
Deactivates tag again:
>>> text, count, skipped = abjad.deactivate(text, abjad.Tag("RED_MARKUP"))
>>> print(text)
\new Staff
{
c'4
%! RED_MARKUP
%%% - \markup { \with-color #red Allegro }
d'4
e'4
f'4
}
>>> lines = [_.strip("\n") for _ in text.split("\n")]
>>> string = "\n".join(lines)
>>> lilypond_file = abjad.LilyPondFile([string])
>>> abjad.show(lilypond_file) # doctest: +SKIP
"""
assert isinstance(text, str), repr(text)
assert isinstance(tag, Tag) or callable(tag), repr(tag)
count, skipped_count = 0, 0
treated_last_line = False
found_already_deactivated_on_last_line = False
previous_line_was_tweak = False
text_lines = text.split("\n")
text_lines = [_ + "\n" for _ in text_lines[:-1]] + text_lines[-1:]
lines, current_tags = [], []
for line in text_lines:
if line.lstrip().startswith("%! "):
lines.append(line)
current_tag = Tag(line.strip()[3:])
current_tags.append(current_tag)
continue
if not _match_line(line, tag, current_tags):
lines.append(line)
treated_last_line = False
found_already_deactivated_on_last_line = False
current_tags = []
continue
start_column = len(line) - len(line.lstrip())
if line[start_column] != "%":
if " %@%" in line:
prefix = " " + "%@% "
line = line.replace(" %@%", "")
else:
prefix = "%%% "
if prepend_empty_chord and not previous_line_was_tweak:
prefix += "<> "
target = line[start_column - 4 : start_column]
assert target == " ", repr((line, target, start_column, tag))
characters = list(line)
characters[start_column - 4 : start_column] = list(prefix)
line = "".join(characters)
if not treated_last_line:
count += 1
treated_last_line = True
found_already_deactivated_on_last_line = False
else:
if not found_already_deactivated_on_last_line:
skipped_count += 1
found_already_deactivated_on_last_line = True
treated_last_line = False
lines.append(line)
previous_line_was_tweak = "tweak" in line
current_tags = []
text = "".join(lines)
return text, count, skipped_count
[docs]
def double_tag(strings: list[str], tag_: Tag, deactivate: bool = False) -> list[str]:
"""
Double tags ``strings``.
"""
assert all(isinstance(_, str) for _ in strings), repr(strings)
assert isinstance(tag_, Tag), repr(tag_)
half_indent = 2 * " "
tag_lines = []
if tag_.string:
tag_lines_ = tag_.string.split(":")
tag_lines_ = [half_indent + "%! " + _ for _ in tag_lines_]
tag_lines.extend(tag_lines_)
tag_lines.sort()
if deactivate is True:
strings = ["%@% " + _ for _ in strings]
result = []
for string in strings:
if string.strip().startswith("%!"):
result.append(string)
continue
result.extend(tag_lines)
result.append(string)
return result