Source code for panflute.base

"""
Base classes and methods of all Pandoc elements
"""

# ---------------------------
# Imports
# ---------------------------

from operator import attrgetter
from collections.abc import MutableSequence, MutableMapping

from .containers import ListContainer, DictContainer
from .utils import check_type, encode_dict  # check_group


# ---------------------------
# Meta Classes
# ---------------------------


[docs]class Element(object): """ Base class of all Pandoc elements """ __slots__ = ['parent', 'location', 'index'] _children = [] def __new__(cls, *args, **kwargs): # This is just to initialize self.parent to None element = object.__new__(cls) element.parent = None element.location = None element.index = None return element @property def tag(self): tag = type(self).__name__ return tag def __eq__(self, other): # Doc has a different method b/c it uses __dict__ instead of slots if type(self) != type(other): return False for key in self.__slots__: if getattr(self, key) != getattr(other, key): return False return True # --------------------------- # Base methods # --------------------------- # Should be overridden except for trivial elements (Space, Null, etc.) def __repr__(self): # This is just a convenience method # Override it for more complex elements extra = [] for key in self.__slots__: if not key.startswith('_') and key != 'text': val = getattr(self, key) if val not in ([], dict(), ''): extra.append([key, val]) if extra: extra = ('{}={}'.format(k, repr(v)) for k, v in extra) extra = '; ' + ', '.join(x for x in extra) else: extra = '' if '_content' in self.__slots__: content = ' '.join(repr(x) for x in self.content) return '{}({}{})'.format(self.tag, content, extra) elif 'text' in self.__slots__: return '{}({}{})'.format(self.tag, self.text, extra) else: return self.tag def to_json(self): return encode_dict(self.tag, self._slots_to_json()) def _slots_to_json(self): # Default when the element contains nothing return [] # --------------------------- # .identifier .classes .attributes # --------------------------- def _set_ica(self, identifier, classes, attributes): self.identifier = check_type(identifier, str) self.classes = [check_type(cl, str) for cl in classes] self.attributes = dict(attributes) def _ica_to_json(self): return [self.identifier, self.classes, list(self.attributes.items())] # --------------------------- # .content (setter and getter) # --------------------------- @property def content(self): """ Sequence of :class:`Element` objects (usually either :class:`Block` or :class:`Inline`) that are "children" of the current element. Only available for elements that accept ``*args``. Note: some elements have children in attributes other than ``content`` (such as :class:`.Table` that has children in the header and caption attributes). """ return self._content @content.setter def content(self, value): oktypes = self._content.oktypes value = value.list if isinstance(value, ListContainer) else list(value) self._content = ListContainer(*value, oktypes=oktypes, parent=self) def _set_content(self, value, oktypes): """ Similar to content.setter but when there are no existing oktypes """ if value is None: value = [] self._content = ListContainer(*value, oktypes=oktypes, parent=self) # --------------------------- # Navigation # --------------------------- @property def container(self): """ Rarely used attribute that returns the ``ListContainer`` or ``DictContainer`` that contains the element (or returns None if no such container exist) :rtype: ``ListContainer`` | ``DictContainer`` | ``None`` """ if self.parent is None: return None elif self.location is None: return self.parent.content else: container = getattr(self.parent, self.location) if isinstance(container, (ListContainer, DictContainer)): return container else: assert self is container # id(self) == id(container)
[docs] def offset(self, n): """ Return a sibling element offset by n :rtype: :class:`Element` | ``None`` """ idx = self.index if idx is not None: sibling = idx + n container = self.container if 0 <= sibling < len(container): return container[sibling]
@property def next(self): """ Return the next sibling. Note that ``elem.offset(1) == elem.next`` :rtype: :class:`Element` | ``None`` """ return self.offset(1) @property def prev(self): """ Return the previous sibling. Note that ``elem.offset(-1) == elem.prev`` :rtype: :class:`Element` | ``None`` """ return self.offset(-1)
[docs] def ancestor(self, n): """ Return the n-th ancestor. Note that ``elem.ancestor(1) == elem.parent`` :rtype: :class:`Element` | ``None`` """ if not isinstance(n, int) or n < 1: raise TypeError('Ancestor needs to be positive, received', n) if n == 1 or self.parent is None: return self.parent else: return self.parent.ancestor(n-1)
# --------------------------- # Walking # --------------------------- @property def doc(self): """ Return the root Doc element (if there is one) """ guess = self while guess is not None and guess.tag != 'Doc': guess = guess.parent # If no parent, this will be None return guess # Returns either Doc or None
[docs] def walk(self, action, doc=None, stop_if=None): """ Walk through the element and all its children (sub-elements), applying the provided function ``action``. A trivial example would be: .. code-block:: python from panflute import * def no_action(elem, doc): pass doc = Doc(Para(Str('a'))) altered = doc.walk(no_action) :param action: function that takes (element, doc) as arguments. :type action: :class:`function` :param doc: root document; used to access metadata, the output format (in ``.format``, other elements, and other variables). Only use this variable if for some reason you don't want to use the current document of an element. :type doc: :class:`.Doc` :param stop_if: function that takes (element) as argument. :type stop_if: :class:`function`, optional :rtype: :class:`Element` | ``[]`` | ``None`` """ # Infer the document thanks to .parent magic if doc is None: doc = self.doc # First iterate over children; unless the stop condition is met if stop_if is None or not stop_if(self): # self._children has property *names* so we need a bit of getattr/setattr magic to modify the objects themselves children = ((child_name, getattr(self, child_name)) for child_name in self._children) for child_name, child in children: if isinstance(child, (Element, ListContainer, DictContainer)): child = child.walk(action, doc, stop_if) elif child is None: child = None # Empty table headers or captions else: raise TypeError(type(child)) setattr(self, child_name, child) # Then apply the action() to the root element altered = action(self, doc) return self if altered is None else altered
[docs]class Inline(Element): """ Base class of all inline elements """ __slots__ = []
[docs]class Block(Element): """ Base class of all block elements """ __slots__ = []
[docs]class MetaValue(Element): """ Base class of all metadata elements """ __slots__ = []