From 2eb26c55320de35a834618abc0c870fefa56edaa Mon Sep 17 00:00:00 2001 From: Joscha Date: Mon, 22 Apr 2019 23:13:34 +0000 Subject: [PATCH] Rename and add attribute setting/getting --- cheuph/markup.py | 129 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 108 insertions(+), 21 deletions(-) diff --git a/cheuph/markup.py b/cheuph/markup.py index 7fa40c3..8348633 100644 --- a/cheuph/markup.py +++ b/cheuph/markup.py @@ -1,7 +1,6 @@ -from typing import Any, Dict, List, Optional, Union - -__all__ = ["Attributes", "Chunk", "MarkedUp"] +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union +__all__ = ["Attributes", "Chunk", "AttributedText"] Attributes = Dict[str, Any] @@ -58,7 +57,7 @@ class Chunk: return self._text @property - def attrs(self) -> Attributes: + def attributes(self) -> Attributes: return dict(self._attributes) # Private methods @@ -66,17 +65,20 @@ class Chunk: def _join(self, chunk: "Chunk") -> Optional["Chunk"]: if self._attributes == chunk._attributes: return Chunk(self.text + chunk.text, self._attributes) - else: - return None + + return None # Public methods - def with_attribute(self, name: str, value: Any) -> "Chunk": + def get(self, name: str, default: Any = None) -> Any: + return self.attributes.get(name, default) + + def set(self, name: str, value: Any) -> "Chunk": new_attributes = dict(self._attributes) new_attributes[name] = value return Chunk(self.text, new_attributes) - def without_attribute(self, name: str) -> "Chunk": + def remove(self, name: str) -> "Chunk": new_attributes = dict(self._attributes) # This removes the value with that key, if it exists, and does nothing @@ -87,16 +89,16 @@ class Chunk: return Chunk(self.text, new_attributes) -class MarkedUp: +class AttributedText: """ Objects of this class are immutable and behave str-like. Supported operations are len, + and splicing. """ @classmethod - def from_chunks(cls, chunks: List[Chunk]) -> "MarkedUp": + def from_chunks(cls, chunks: Iterable[Chunk]) -> "AttributedText": new = cls() - new._chunks = Chunk.join_chunks(chunks) + new._chunks = Chunk.join_chunks(list(chunks)) return new # Common special methods @@ -107,17 +109,17 @@ class MarkedUp: self._chunks.append(Chunk(text)) def __str__(self) -> str: - return "".join(map(str, self._chunks)) + return self.text def __repr__(self) -> str: - return f"MarkedUp.from_chunks({self._chunks!r})" + return f"AttributedText.from_chunks({self._chunks!r})" # Uncommon special methods - def __add__(self, other: "MarkedUp") -> "MarkedUp": - return MarkedUp.from_chunks(self._pieces + other._pieces) + def __add__(self, other: "AttributedText") -> "AttributedText": + return AttributedText.from_chunks(self._chunks + other._chunks) - def __getitem__(self, key: Union[int, slice]) -> "MarkedUp": + def __getitem__(self, key: Union[int, slice]) -> "AttributedText": chunks: List[Chunk] if isinstance(key, slice): @@ -125,13 +127,17 @@ class MarkedUp: else: chunks = [self._at(key)] - return MarkedUp.from_chunks(chunks) + return AttributedText.from_chunks(chunks) def __len__(self) -> int: return sum(map(len, self._chunks)) # Properties + @property + def text(self) -> str: + return "".join(chunk.text for chunk in self._chunks) + @property def chunks(self) -> List[Chunk]: return list(self._chunks) @@ -150,8 +156,9 @@ class MarkedUp: return chunk[chunk_key] pos += len(chunk) - else: - raise KeyError + + # We haven't found the chunk + raise KeyError def _slice(self, key: slice) -> List[Chunk]: start, stop, step = key.start, key.stop, key.step @@ -194,5 +201,85 @@ class MarkedUp: # Public methods - def attrs(self, key: int) -> Attributes: - return self._at(key).attrs + def at(self, pos: int) -> Attributes: + return self._at(pos).attributes + + def get(self, + pos: int, + name: str, + default: Any = None, + ) -> Any: + return self._at(pos).get(name, default) + + # "find all separate blocks with this property" + def find_all(self, name: str) -> List[Tuple[Any, int, int]]: + blocks = [] + pos = 0 + block = None + + for chunk in self._chunks: + if name in chunk.attributes: + attribute = chunk.attributes[name] + start = pos + stop = pos + len(chunk) + + if block is None: + block = (attribute, start, stop) + continue + + block_attr, block_start, _ = block + + if block_attr == attribute: + block = (attribute, block_start, stop) + else: + blocks.append(block) + block = (attribute, start, stop) + + pos += len(chunk) + + if block is not None: + blocks.append(block) + + return blocks + + def set(self, + name: str, + value: Any, + start: Optional[int] = None, + stop: Optional[int] = None, + ) -> "AttributedText": + if start is None and stop is None: + chunks = (chunk.set(name, value) for chunk in self._chunks) + return AttributedText.from_chunks(chunks) + elif start is None: + return self[:stop].set(name, value) + self[stop:] + elif stop is None: + return self[:start] + self[start:].set(name, value) + elif start > stop: + # set value everywhere BUT the specified interval + return self.set(name, value, stop=stop).set(name, value, start=start) + else: + middle = self[start:stop].set(name, value) + return self[:start] + middle + self[stop:] + + def set_at(self, name: str, value: Any, pos: int) -> "AttributedText": + return self.set(name, value, pos, pos) + + def remove(self, + name: str, + start: Optional[int] = None, + stop: Optional[int] = None, + ) -> "AttributedText": + if start is None and stop is None: + chunks = (chunk.remove(name) for chunk in self._chunks) + return AttributedText.from_chunks(chunks) + elif start is None: + return self[:stop].remove(name) + self[stop:] + elif stop is None: + return self[:start] + self[start:].remove(name) + elif start > stop: + # remove value everywhere BUT the specified interval + return self.remove(name, stop=stop).remove(name, start=start) + else: + middle = self[start:stop].remove(name) + return self[:start] + middle + self[stop:]