# -*- 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/>.
"""
Some general element types and some base classes for the quickly.dom elements.
"""
### NOTE: if adding classes/functions here, update docs/source/dom/base.rst!
import re
import parce.action as a
from parce import lexicon, transform
from . import element
## Base classes:
[docs]class Document(element.Element):
"""Base class for a full source document."""
space_between = '\n\n'
[docs] def concat_space(self, n, m):
if isinstance(n, (SinglelineComment, Newline)):
return '\n'
return self.space_between
[docs]class String(element.TextElement):
"""Base class for a string element."""
[docs] @classmethod
def read_head(cls, origin):
return ''.join(t.text[1:] if t.action is a.String.Escape else t.text
for t in origin[1:-1])
[docs] def write_head(self):
return '"{}"'.format(re.sub(r'([\\"])', r'\\\1', self.head))
[docs]class BackslashCommand(element.TextElement):
r"""A command that starts with a backslash, like in LaTeX and LilyPond.
The backslash (``\``) is not in the head value.
"""
[docs] @classmethod
def check_head(cls, head):
r"""Return False if the head value starts with a backslash (``\``)."""
return not head.startswith('\\')
[docs] @classmethod
def read_head(cls, origin):
"""Strip the backslash of the origin token."""
text = ''.join(t.text for t in origin)[1:]
return text
[docs] def write_head(self):
"""Add the backslash on write-out."""
return '\\' + self.head
## Generic elements:
[docs]class Newline(element.Element):
"""A Newline.
Not created from existing documents, but you can insert this node
anywhere you want a newline in manually crafted documents.
"""
head = ''
space_after = '\n'
[docs]class BlankLine(element.Element):
"""A blank line.
Not created from existing documents, but you can insert this node
anywhere you want a blank line in manually crafted documents.
"""
head = ''
space_after = '\n\n'
[docs]class Line(element.Element):
"""Container that prints the child nodes on one line with a space in between.
Not created from existing documents, but you can insert this node in a
Document when you want some nodes to be on the same line, for example when
you want to write a comment at the end of the preceding line instead of on
a line of its own.
"""
space_before = space_after = '\n'
space_between = ' '
[docs]class Column(element.Element):
"""Container that prints every child node on a new line.
Not created from existing documents, but you can insert this node in a
Document when you want some nodes to be stacked vertically.
"""
space_before = space_after = space_between = '\n'
[docs]class Text(element.TextElement):
"""Generic text that is printed unmodified."""
## Special element:
[docs]class Unknown(element.HeadElement):
"""Represents a document region that is not transformed.
This element can only occur in documents transformed from source. It is
used to denote reqions that are not transformed, such as CSS style tags or
attributes, or script tags, in Html documents containing LilyPond music.
*Parce* has fancy highlighting for those text fragments, but it makes no
sense to try to transform those also to useful DOM nodes. So instead, we
simply record the positions in the source document of these fragments using
the first and the last token of such contexts that are not transformed.
Calling :meth:`write_head` on this element results in an exception, because
it does not know how it looks. But it knows the position in the document,
because the first and the last untransformed tokens are in the origin.
Before you can write out a document containing :class:`Unknown` elements
fully out (e.g. using :meth:`~.element.Element.write` or
:meth:`~.element.Element.write_indented`), you should replace those
elements with e.g. :class:`Text` elements that have the text such as it
appears in the source document. This can be done using
:func:`.util.replace_unknown`.
You *can* :meth:`~.element.Element.edit` documents with this element however,
it will simply leave the unknown parts of the document as they are.
"""
[docs] def write_head(self):
"""Raise RuntimeError."""
raise RuntimeError(
"can't write head value of Unknown element.\n"
"Hint: replace Unknown with Text elements.")
## Language and Transform base/helper classes
[docs]class XmlLike:
"""Mixin class for a language definition that bases on parce.lang.Xml.
Adds the comsume attribute to some lexicons, like comment and string, which
makes transforming easier.
"""
@lexicon(consume=True)
def comment(cls):
yield from super().comment
@lexicon(consume=True)
def sqstring(cls):
yield from super().sqstring
@lexicon(consume=True)
def dqstring(cls):
yield from super().dqstring
@lexicon(consume=True)
def cdata(cls):
yield from super().cdata
@lexicon(consume=True)
def processing_instruction(cls):
yield from super().processing_instruction