203 lines
5.5 KiB
Python
203 lines
5.5 KiB
Python
# Copyright (c) 2010-2024 openpyxl
|
|
|
|
"""
|
|
RichText definition
|
|
"""
|
|
from copy import copy
|
|
from openpyxl.compat import NUMERIC_TYPES
|
|
from openpyxl.cell.text import InlineFont, Text
|
|
from openpyxl.descriptors import (
|
|
Strict,
|
|
String,
|
|
Typed
|
|
)
|
|
|
|
from openpyxl.xml.functions import Element, whitespace
|
|
|
|
class TextBlock(Strict):
|
|
""" Represents text string in a specific format
|
|
|
|
This class is used as part of constructing a rich text strings.
|
|
"""
|
|
font = Typed(expected_type=InlineFont)
|
|
text = String()
|
|
|
|
def __init__(self, font, text):
|
|
self.font = font
|
|
self.text = text
|
|
|
|
|
|
def __eq__(self, other):
|
|
return self.text == other.text and self.font == other.font
|
|
|
|
|
|
def __str__(self):
|
|
"""Just retun the text"""
|
|
return self.text
|
|
|
|
|
|
def __repr__(self):
|
|
font = self.font != InlineFont() and self.font or "default"
|
|
return f"{self.__class__.__name__} text={self.text}, font={font}"
|
|
|
|
|
|
def to_tree(self):
|
|
el = Element("r")
|
|
el.append(self.font.to_tree(tagname="rPr"))
|
|
t = Element("t")
|
|
t.text = self.text
|
|
whitespace(t)
|
|
el.append(t)
|
|
return el
|
|
|
|
#
|
|
# Rich Text class.
|
|
# This class behaves just like a list whose members are either simple strings, or TextBlock() instances.
|
|
# In addition, it can be initialized in several ways:
|
|
# t = CellRFichText([...]) # initialize with a list.
|
|
# t = CellRFichText((...)) # initialize with a tuple.
|
|
# t = CellRichText(node) # where node is an Element() from either lxml or xml.etree (has a 'tag' element)
|
|
class CellRichText(list):
|
|
"""Represents a rich text string.
|
|
|
|
Initialize with a list made of pure strings or :class:`TextBlock` elements
|
|
Can index object to access or modify individual rich text elements
|
|
it also supports the + and += operators between rich text strings
|
|
There are no user methods for this class
|
|
|
|
operations which modify the string will generally call an optimization pass afterwards,
|
|
that merges text blocks with identical formats, consecutive pure text strings,
|
|
and remove empty strings and empty text blocks
|
|
"""
|
|
|
|
def __init__(self, *args):
|
|
if len(args) == 1:
|
|
args = args[0]
|
|
if isinstance(args, (list, tuple)):
|
|
CellRichText._check_rich_text(args)
|
|
else:
|
|
CellRichText._check_element(args)
|
|
args = [args]
|
|
else:
|
|
CellRichText._check_rich_text(args)
|
|
super().__init__(args)
|
|
|
|
|
|
@classmethod
|
|
def _check_element(cls, value):
|
|
if not isinstance(value, (str, TextBlock, NUMERIC_TYPES)):
|
|
raise TypeError(f"Illegal CellRichText element {value}")
|
|
|
|
|
|
@classmethod
|
|
def _check_rich_text(cls, rich_text):
|
|
for t in rich_text:
|
|
CellRichText._check_element(t)
|
|
|
|
@classmethod
|
|
def from_tree(cls, node):
|
|
text = Text.from_tree(node)
|
|
if text.t:
|
|
return (text.t.replace('x005F_', ''),)
|
|
s = []
|
|
for r in text.r:
|
|
t = ""
|
|
if r.t:
|
|
t = r.t.replace('x005F_', '')
|
|
if r.rPr:
|
|
s.append(TextBlock(r.rPr, t))
|
|
else:
|
|
s.append(t)
|
|
return cls(s)
|
|
|
|
# Merge TextBlocks with identical formatting
|
|
# remove empty elements
|
|
def _opt(self):
|
|
last_t = None
|
|
l = CellRichText(tuple())
|
|
for t in self:
|
|
if isinstance(t, str):
|
|
if not t:
|
|
continue
|
|
elif not t.text:
|
|
continue
|
|
if type(last_t) == type(t):
|
|
if isinstance(t, str):
|
|
last_t += t
|
|
continue
|
|
elif last_t.font == t.font:
|
|
last_t.text += t.text
|
|
continue
|
|
if last_t:
|
|
l.append(last_t)
|
|
last_t = t
|
|
if last_t:
|
|
# Add remaining TextBlock at end of rich text
|
|
l.append(last_t)
|
|
super().__setitem__(slice(None), l)
|
|
return self
|
|
|
|
|
|
def __iadd__(self, arg):
|
|
# copy used here to create new TextBlock() so we don't modify the right hand side in _opt()
|
|
CellRichText._check_rich_text(arg)
|
|
super().__iadd__([copy(e) for e in list(arg)])
|
|
return self._opt()
|
|
|
|
|
|
def __add__(self, arg):
|
|
return CellRichText([copy(e) for e in list(self) + list(arg)])._opt()
|
|
|
|
|
|
def __setitem__(self, indx, val):
|
|
CellRichText._check_element(val)
|
|
super().__setitem__(indx, val)
|
|
self._opt()
|
|
|
|
|
|
def append(self, arg):
|
|
CellRichText._check_element(arg)
|
|
super().append(arg)
|
|
|
|
|
|
def extend(self, arg):
|
|
CellRichText._check_rich_text(arg)
|
|
super().extend(arg)
|
|
|
|
|
|
def __repr__(self):
|
|
return "CellRichText([{}])".format(', '.join((repr(s) for s in self)))
|
|
|
|
|
|
def __str__(self):
|
|
return ''.join([str(s) for s in self])
|
|
|
|
|
|
def as_list(self):
|
|
"""
|
|
Returns a list of the strings contained.
|
|
The main reason for this is to make editing easier.
|
|
"""
|
|
return [str(s) for s in self]
|
|
|
|
|
|
def to_tree(self):
|
|
"""
|
|
Return the full XML representation
|
|
"""
|
|
container = Element("is")
|
|
for obj in self:
|
|
if isinstance(obj, TextBlock):
|
|
container.append(obj.to_tree())
|
|
|
|
else:
|
|
el = Element("r")
|
|
t = Element("t")
|
|
t.text = obj
|
|
whitespace(t)
|
|
el.append(t)
|
|
container.append(el)
|
|
|
|
return container
|
|
|