Source code for quickly.dom.edit

# -*- coding: utf-8 -*-
# This file is part of `quickly`, a library for LilyPond and the `.ly` format
# Copyright © 2019-2020 by Wilbert Berendsen <>
# 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
# 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 <>.

The Edit base class, to perform operations on a DOM document and/or a related
parce Document in different ways.

import parce.document

from ..node import Range
from .element import Element

[docs]class Edit: """Base class to perform operations on a DOM document via a :class:`~quickly.node.Range`, an :class:`.element.Element` node, a :class:`parce.Document` or a selection of a parce document in a :class:`parce.Cursor`. You must implement at least :meth:`Edit.edit_range` to make it work. You can choose to reimplement other methods to alter behaviour or functionality. Then you create an instance, and you can call one of the ``edit_xxx()`` methods. """ _document = None # The parce document, if available _changes = 0 # The result of Element.edit(document) #: If True, when there is a selection, a Range is created from the root #: node, otherwise from the younghest common ancestor. range_from_root = False #: If True, a Range is created from the cursor's position to the end, #: instead of the full document in case there is no selection. range_from_cursor = False #: If True, does not write back changes to the parce Document readonly = False
[docs] def document(self): """Return the parce Document, if available. This is the document that was used when :meth:`edit_cursor` or :meth:`edit_document` was called. The document is only available during that edit call. """ return self._document
[docs] def changes(self): """Return the number of changes made to the parce Document.""" return self._changes
[docs] def find_block(self, node): """The parce Block of the node (None if there is no parce Document).""" if node.pos is not None: doc = self.document() if doc: return doc.find_block(node.pos)
[docs] def edit(self, music): """Convenience method calling one of the other edit_xxx methods depending on the type.""" meth = (self.edit_cursor if isinstance(music, parce.Cursor) else self.edit_document if isinstance(music, parce.document.AbstractDocument) else self.edit_range if isinstance(music, Range) else self.edit_node if isinstance(music, Element) else None) if meth: return meth(music) raise TypeError('unknown music type')
[docs] def edit_cursor(self, cursor): """Edit the range pointed to by the :class:`parce.Cursor`. The default implementation calls :meth:`edit_range` with a Range, that by default encompasses the full DOM tree when there is no selection. Set :attr:`range_from_cursor` to True if you want the edited range to be from the cursor's position to the document end when there is no selection. If the cursor has a selection, the Range encompasses only the child nodes that are within the cursor's selection. The ancestor of the range is by default the younghest common ancestor of the start and end nodes of the range. Set :attr:`range_from_root` to True if you want the ancestor of the range to be the DOM tree root anyway. """ d = cursor.document().get_transform(True) r = start = end = None if cursor.has_selection(): start_node = d.find_descendant_right(cursor.pos) end_node = d.find_descendant_left(cursor.end) if end_node or start_node: r = Range.from_nodes(start_node, end_node, self.range_from_root) if start_node: start = start_node.pos if end_node: end = end_node.end elif self.range_from_cursor: start_node = d.find_descendant_right(cursor.pos) if start_node: r = Range.from_nodes(start_node) if r is None: r = Range(d) self._document = cursor.document() self._changes = 0 result = self.edit_range(r) if not self.readonly: self._changes = r.ancestor().edit(cursor.document(), start=start, end=end) self._document = None return result
[docs] def edit_document(self, document): """Edit the full :class:`parce.Document`. The default implementation calls :meth:`edit_cursor` with a :class:`parce.Cursor` pointing to the beginning of the document, without selection. """ return self.edit_cursor(parce.Cursor(document))
[docs] def edit_node(self, node): """Edit the full :class:`.element.Element` node. The default implementation calls :meth:`edit_range` with a Range encompassing the full element node. """ return self.edit_range(Range(node))
[docs] def edit_range(self, r): """Edit the specified :class:`~quickly.node.Range`. At least this method needs to be implemented to actually perform the operation. """ raise NotImplementedError