Source code for quickly.dom.indent

# -*- 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/>.


"""
Functionality to pretty-print a DOM document, with good default indentation.

"""

from . import util


class _IndentLevel:
    """Holds the information for a new indent level."""
    def __init__(self, node):
        self.align_indices = tuple(node.indent_align_indices())
        self.align_positions = {}

    def align_child(self, child_index, pos):
        """Store a position value for the child at the specified index."""
        self.align_positions[child_index] = pos

    def get_align_pos(self):
        """Get a stored align value if the node supports it.

        Otherwise, None is returned.

        """
        for index in self.align_indices:
            try:
                return self.align_positions[index]
            except KeyError:
                pass


class _Block:
    """Keeps the administration of a line of output text.

    The number of spaces to indent this line is in the ``indent`` attribute;
    the line itself is built as a list in the ``line`` attribute.

    """
    def __init__(self, indent):
        self.indent = indent
        self.line = []

    def offset(self, pos):
        """Get the total length of the text in the first ``pos`` pieces."""
        return sum(map(len, self.line[:pos]))

    def output(self):
        """Get the output line."""
        return ' ' * self.indent + ''.join(self.line) + '\n'


[docs]class Indenter: """Prints the indented output of a node. Indentation preferences can be given on instantiation or by setting the attributes of the same name. The default ``indent_width`` can be given, and the additional ``start_indent`` which is prepended to every output line, both in number of spaces and defaulting to 0. The ``max_align_indent`` argument determines the number of spaces used at most to align indenting lines with text on previous lines. If the number is exceeded, the default ``indent_width`` is used instead on such lines. Call :meth:`write` to get the indented text output of a node. """ def __init__(self, indent_width = 2, start_indent = 0, max_align_indent = 16, ): #: the default indent width self.indent_width = indent_width #: the number of spaces to prepend to every output line self.start_indent = start_indent #: the maximum number of spaces to indent to align a line with certain text on the previous line self.max_align_indent = max_align_indent # initialize working variables self._output = [] # the list in which the result output is built up self._indent_stack = [] # the list of indenting history self._indenters = [] # the list of indenters created in the current line self._dedenters = 0 # the negative count of indent levels to end self._whitespace = [] # collects the minimal amount of whitespace between nodes self._can_dedent = False # can the current line be dedented
[docs] def write(self, node): """Get the indented output of the node. Called by :meth:`Element.write_indented() <quickly.dom.element.Element.write_indented>`. """ self._output.clear() self._indent_stack.clear() self._indenters.clear() self._dedenters = 0 self._whitespace.clear() self.create_new_block() self.output_node(node) # strip preceding space result = self._output while result: if result[0].line: if result[0].line[0].isspace(): del result[0].line[0] else: break else: del result[0] return ''.join(block.output() for block in result)
[docs] def output_node(self, node, index=-1): """*(Internal.)* Output one node and its children. The index, if given, is the index of this node in its parent. This is used to get additional indenting hints for the node. """ head = node.write_head() tail = node.write_tail() has_children = len(node) > 0 indent = node.indent_children() self.add_whitespace(node.space_before) if head: self.output_head(head, index, node.indent_override()) if tail or has_children: self.add_whitespace(node.space_after_head) if indent: self.enter_indent(node) if has_children: n = node[0] self.output_node(n, 0) for i, m in enumerate(node[1:], 1): self.add_whitespace(node.concat_space(n, m)) self.output_node(m, i) n = m if indent: self.leave_indent() if tail: self.add_whitespace(node.space_before_tail) self.output_tail(tail) self.add_whitespace(node.space_after)
[docs] def add_whitespace(self, whitespace): """*(Internal.)* Add whitespace, which is combined as soon as text is printed out.""" self._whitespace.append(whitespace)
[docs] def enter_indent(self, node): """*(Internal.)* Enter a new indent level for the ``node``.""" self._indenters.append(_IndentLevel(node))
[docs] def leave_indent(self): """*(Internal.)* Leave the younghest indent level.""" if self._indenters: self._indenters.pop() else: self._dedenters -= 1
[docs] def current_indent(self): """*(Internal.)* Get the current indent (including ``start_indent``) in nr of spaces. """ return self._indent_stack[-1] if self._indent_stack else self.start_indent
[docs] def create_new_block(self): """*(Internal.)* Go to a new line.""" if self._dedenters: # remove some levels del self._indent_stack[self._dedenters:] self._dedenters = 0 if self._indenters: # add new indent levels new_indent = current_indent = self.current_indent() for i in self._indenters: new_indent += self.indent_width pos = i.get_align_pos() if pos is not None: align_indent = self._output[-1].offset(pos) if align_indent <= self.max_align_indent: new_indent = current_indent + align_indent self._indent_stack.append(new_indent) self._indenters.clear() current_indent = self.current_indent() self._can_dedent = bool(self._indent_stack) self._output.append(_Block(current_indent))
[docs] def output_space(self): """*(Internal.)* Output whitespace. Newlines start a new output line.""" for c in util.collapse_whitespace(self._whitespace): if c == '\n': self.create_new_block() else: self._output[-1].line.append(c) self._whitespace.clear()
[docs] def output_head(self, text, index=-1, override=None): """*(Internal.)* Output head text. The ``index``, if given, is the index of the node in its parent. This is used to get additional indenting hints for the node. If ``override`` is not None, and this head text happens to be the first on a new line, this value is used as the indent depth for this line, in stead of the current indent. """ self.output_space() self._can_dedent = False last_line = self._output[-1].line if not last_line and override is not None: self._output[-1].indent = override if self._indenters and index in self._indenters[-1].align_indices: # store the position of the node on the current output line position = len(last_line) self._indenters[-1].align_child(index, position) last_line.append(text)
[docs] def output_tail(self, text): """*(Internal.)* Output the tail text.""" self.output_space() if self._can_dedent and self._dedenters: del self._indent_stack[self._dedenters] self._dedenters = 0 self._output[-1].indent = self.current_indent() self._output[-1].line.append(text)