1# Copyright (c) 2010-2024 openpyxl
2
3"""
4RichText definition
5"""
6from copy import copy
7from openpyxl.compat import NUMERIC_TYPES
8from openpyxl.cell.text import InlineFont, Text
9from openpyxl.descriptors import (
10 Strict,
11 String,
12 Typed
13)
14
15from openpyxl.xml.functions import Element, whitespace
16
17class TextBlock(Strict):
18 """ Represents text string in a specific format
19
20 This class is used as part of constructing a rich text strings.
21 """
22 font = Typed(expected_type=InlineFont)
23 text = String()
24
25 def __init__(self, font, text):
26 self.font = font
27 self.text = text
28
29
30 def __eq__(self, other):
31 return self.text == other.text and self.font == other.font
32
33
34 def __str__(self):
35 """Just retun the text"""
36 return self.text
37
38
39 def __repr__(self):
40 font = self.font != InlineFont() and self.font or "default"
41 return f"{self.__class__.__name__} text={self.text}, font={font}"
42
43
44 def to_tree(self):
45 el = Element("r")
46 el.append(self.font.to_tree(tagname="rPr"))
47 t = Element("t")
48 t.text = self.text
49 whitespace(t)
50 el.append(t)
51 return el
52
53#
54# Rich Text class.
55# This class behaves just like a list whose members are either simple strings, or TextBlock() instances.
56# In addition, it can be initialized in several ways:
57# t = CellRFichText([...]) # initialize with a list.
58# t = CellRFichText((...)) # initialize with a tuple.
59# t = CellRichText(node) # where node is an Element() from either lxml or xml.etree (has a 'tag' element)
60class CellRichText(list):
61 """Represents a rich text string.
62
63 Initialize with a list made of pure strings or :class:`TextBlock` elements
64 Can index object to access or modify individual rich text elements
65 it also supports the + and += operators between rich text strings
66 There are no user methods for this class
67
68 operations which modify the string will generally call an optimization pass afterwards,
69 that merges text blocks with identical formats, consecutive pure text strings,
70 and remove empty strings and empty text blocks
71 """
72
73 def __init__(self, *args):
74 if len(args) == 1:
75 args = args[0]
76 if isinstance(args, (list, tuple)):
77 CellRichText._check_rich_text(args)
78 else:
79 CellRichText._check_element(args)
80 args = [args]
81 else:
82 CellRichText._check_rich_text(args)
83 super().__init__(args)
84
85
86 @classmethod
87 def _check_element(cls, value):
88 if not isinstance(value, (str, TextBlock, NUMERIC_TYPES)):
89 raise TypeError(f"Illegal CellRichText element {value}")
90
91
92 @classmethod
93 def _check_rich_text(cls, rich_text):
94 for t in rich_text:
95 CellRichText._check_element(t)
96
97 @classmethod
98 def from_tree(cls, node):
99 text = Text.from_tree(node)
100 if text.t:
101 return (text.t.replace('x005F_', ''),)
102 s = []
103 for r in text.r:
104 t = ""
105 if r.t:
106 t = r.t.replace('x005F_', '')
107 if r.rPr:
108 s.append(TextBlock(r.rPr, t))
109 else:
110 s.append(t)
111 return cls(s)
112
113 # Merge TextBlocks with identical formatting
114 # remove empty elements
115 def _opt(self):
116 last_t = None
117 l = CellRichText(tuple())
118 for t in self:
119 if isinstance(t, str):
120 if not t:
121 continue
122 elif not t.text:
123 continue
124 if type(last_t) == type(t):
125 if isinstance(t, str):
126 last_t += t
127 continue
128 elif last_t.font == t.font:
129 last_t.text += t.text
130 continue
131 if last_t:
132 l.append(last_t)
133 last_t = t
134 if last_t:
135 # Add remaining TextBlock at end of rich text
136 l.append(last_t)
137 super().__setitem__(slice(None), l)
138 return self
139
140
141 def __iadd__(self, arg):
142 # copy used here to create new TextBlock() so we don't modify the right hand side in _opt()
143 CellRichText._check_rich_text(arg)
144 super().__iadd__([copy(e) for e in list(arg)])
145 return self._opt()
146
147
148 def __add__(self, arg):
149 return CellRichText([copy(e) for e in list(self) + list(arg)])._opt()
150
151
152 def __setitem__(self, indx, val):
153 CellRichText._check_element(val)
154 super().__setitem__(indx, val)
155 self._opt()
156
157
158 def append(self, arg):
159 CellRichText._check_element(arg)
160 super().append(arg)
161
162
163 def extend(self, arg):
164 CellRichText._check_rich_text(arg)
165 super().extend(arg)
166
167
168 def __repr__(self):
169 return "CellRichText([{}])".format(', '.join((repr(s) for s in self)))
170
171
172 def __str__(self):
173 return ''.join([str(s) for s in self])
174
175
176 def as_list(self):
177 """
178 Returns a list of the strings contained.
179 The main reason for this is to make editing easier.
180 """
181 return [str(s) for s in self]
182
183
184 def to_tree(self):
185 """
186 Return the full XML representation
187 """
188 container = Element("is")
189 for obj in self:
190 if isinstance(obj, TextBlock):
191 container.append(obj.to_tree())
192
193 else:
194 el = Element("r")
195 t = Element("t")
196 t.text = obj
197 whitespace(t)
198 el.append(t)
199 container.append(el)
200
201 return container
202