Source code for quickly.dom.scm

# -*- 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 Scheme expressions.

Besides the elements a few functions are provided to make it easier to
manually construct scheme expressions. For example::

    >>> from quickly.dom.scm import q, qq, uq, i, p, s
    >>> s(True)
    <scm.Bool #t>
    >>> s(100)
    <scm.Number 100>
    >>> s(100.123)
    <scm.Number 100.123>
    >>> s(('text', -2)).dump()
    <scm.List (3 children)>
     ├╴<scm.String 'text'>
     ├╴<scm.Dot>
     ╰╴<scm.Number -2>
    >>> s(('text', -2)).write()
    '("text" . -2)'
    >>> q(s((i('text'), -2))).write()
    "'(text . -2)"
    >>> n = s([i('if'), [i('<'), i('a'), 100], "smaller", "larger"])
    >>> n.dump()
    <scm.List (4 children)>
     ├╴<scm.Identifier 'if'>
     ├╴<scm.List (3 children)>
     │  ├╴<scm.Identifier '<'>
     │  ├╴<scm.Identifier 'a'>
     │  ╰╴<scm.Number 100>
     ├╴<scm.String 'smaller'>
     ╰╴<scm.String 'larger'>
    >>> n.write()
    '(if (< a 100) "smaller" "larger")'

"""

import fractions
import math

from parce.lang.scheme import scheme_is_indenting_keyword

from . import base, element


[docs]class LilyPond(element.BlockElement): """A LilyPond block inside Scheme, between ``#{`` and ``#}``.""" head = "#{" tail = "#}"
[docs]class Document(base.Document): """A full Scheme document."""
[docs]class SinglelineComment(base.SinglelineComment): """A singleline comment in Scheme after ``;``.""" space_after = '\n'
[docs] @classmethod def read_head(cls, origin): return ''.join(t.text for t in origin[1:])
[docs] def write_head(self): return ';{}'.format(self.head)
[docs]class MultilineComment(base.MultilineComment): """A multiline comment in Scheme after ``#!``."""
[docs] @classmethod def read_head(cls, origin): end = -1 if origin[-1] == "#!" else None return ''.join(t.text for t in origin[1:end])
[docs] def write_head(self): return '#!{}#!'.format(self.head)
[docs]class Char(element.TextElement): """A Scheme character."""
[docs] @classmethod def read_head(cls, origin): return origin[0].text[2:] # leave out the '#\' prefix
[docs] def write_head(self): return r'#\{}'.format(self.head)
[docs]class String(base.String): """A quoted string."""
[docs]class Identifier(element.TextElement): """A Scheme identifier (keyword, variable, symbol)."""
[docs]class List(element.BlockElement): """A Scheme pair or list ( ... ).""" space_between = " " head = "(" tail = ")"
[docs] def indent_align_indices(self): """How to align child nodes if on a new line.""" for n in self: if isinstance(n, Identifier): if scheme_is_indenting_keyword(n.head): return yield 1 # prefer align at the second item break yield 0
[docs]class Vector(element.BlockElement): """A Scheme vector #( ... ).""" space_between = " " head = "#(" tail = ")"
[docs]class Quote(element.TextElement): r"""A Scheme quote ``'``, ``\```, ``,`` or ``,@``."""
[docs] @classmethod def check_head(cls, head): return head in ("'", "`", ",", ",@")
[docs]class Number(element.TextElement): """A decimal numerical value, and the base class for Hex, Bin, Oct. All features of Scheme numerical values are supported: exact/inexactness, polar coordinates, complex numbers, fractions, infinity, nan and unknown digits (#). """ radix = 10 _prefix = {2: "#b", 8: "#o", 10: "", 16: "#x" } _fmt = {2: "b", 8: "o", 10: "d", 16: "x" }
[docs] @classmethod def read_head(cls, origin): from parce.lang.scheme import scheme_number return scheme_number(origin)
[docs] def write_head(self): v = self.head if v == math.inf: s = '+inf.0' elif v == -math.inf: s = '-inf.0' elif v is math.nan: s = '+nan.0' elif isinstance(v, fractions.Fraction): fmt = self._fmt[self.radix] f = lambda n: format(n, fmt) s = f(v.numerator) if v.denominator != 1: s = '{}/{}'.format(s, f(v.denominator)) elif isinstance(v, float) and self.radix == 10: s = str(v) elif isinstance(v, complex): s = '{}{:+}i'.format(v.real, v.imag) else: s = format(int(v), self._fmt[self.radix]) return self._prefix[self.radix] + s
[docs] def repr_head(self): """Dump Scheme numbers in Scheme syntax.""" return self.write_head()
[docs]class Bin(Number): """A Scheme binary integer value.""" radix = 2
[docs]class Oct(Number): """A Scheme octal integer value.""" radix = 8
[docs]class Hex(Number): """A Scheme hexadecimal integer value.""" radix = 16
[docs]class Bool(Number): """A Scheme boolean value."""
[docs] @classmethod def read_head(cls, origin): return origin[0].text[1] in 'tT'
[docs] def write_head(self): return '#t' if self.head else '#f'
[docs]class NaN(Number): """Not a Number, created when a ``number`` context has invalid tokens."""
[docs] @classmethod def read_head(cls, origin): return math.nan
[docs]class Dot(element.HeadElement): """A dot, e.g. in a Scheme pair.""" head = '.'
[docs]def create_element_from_value(value): """Convert a regular Python value to a scheme Element node. Python bool, int, float or str values are converted into Bool, Number, or String objects respectively. A list is converted into a List element, and a tuple (of length > 1) in a pair, with a dot inserted before the last node. Element objects are returned unchanged. A KeyError is raised when there is no conversion for the value's type. """ if isinstance(value, element.Element): return value return _element_mapping[type(value)](value)
[docs]def q(arg): """Quote arg. Automatically converts arguments if needed.""" return Quote("'", create_element_from_value(arg))
[docs]def qq(arg): """Quasi-quote arg. Automatically converts arguments if needed.""" return Quote("`", create_element_from_value(arg))
[docs]def uq(arg): """Un-quote arg. Automatically converts arguments if needed.""" return Quote(",", create_element_from_value(arg))
[docs]def i(arg): """Make an identifier of str arg.""" return Identifier(arg)
[docs]def p(arg1, arg2, *args): """Return a pair (two-or-more element List with dot before last element).""" return create_element_from_value((arg1, arg2, *args))
[docs]def s(arg): """Same as :func:`create_element_from_value`.""" return create_element_from_value(arg)
# used in the create_element_from_value function _element_mapping = { bool: Bool, int: Number, float: Number, fractions.Fraction: Number, str: String, list: (lambda value: List(*map(s, value))), tuple: (lambda value: List(*map(s, value[:-1]), Dot(), *map(s, value[-1:]))), }